@byokit/openclaw 0.17.0 → 0.18.3

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,23 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.18.3 (2026-10-19)
6
+
7
+ - A signed-in Claude Code login is an account in the kit's account list, and its Claude CLI route's models are listed under it.
8
+
9
+ ## 0.18.2 (2026-10-19)
10
+
11
+ - A signed-in ChatGPT login lists the Codex CLI route's models in the account model list, marked available.
12
+
13
+ ## 0.18.1 (2026-10-18)
14
+
15
+ - Dependency update: pins @byokit/accounts 0.28.1.
16
+ - FIX: (from @byokit/accounts 0.28.1) Installed Codex readiness retries a transient account check once and distinguishes timeouts, startup, pipe, exit, protocol and unreadable-reply failures from a confirmed signed-out account, with safe typed probe metadata and no provider messages or stderr.
17
+
18
+ ## 0.18.0 (2026-10-18)
19
+
20
+ - Reattach a paired device to a live-kit run by its existing idempotency key: sequenced replay then live events, a bounded journal with typed truncation, and grant-member isolation. Device disconnect does not interrupt the run; engine-restart reconstruction is not included.
21
+
5
22
  ## 0.17.0 (2026-10-18)
6
23
 
7
24
  - Add opt-in `RunSpec.delegates` for native `sessions_send`/`sessions_spawn` app-tool admission and typed delegated events, retained exact receipts and subtree cancellation. Targets retain their own accounts; helper/person account sharing is not included.
@@ -2,6 +2,7 @@ import { createLocks } from './locks.ts';
2
2
  import { type RouteView } from './routes.ts';
3
3
  import type { Account, AccountId, AccountPick, Defaults, GatewayTransport, Member, ModelInfo, MoveResult, Room, RunSelection, SignInOptions, SignInView } from './types.ts';
4
4
  export declare const modelsOf: (provider: string, answer: unknown) => ModelInfo[];
5
+ export declare const modelsOfAccount: (row: Pick<Account, 'provider' | 'state'>, answer: unknown) => ModelInfo[];
5
6
  type IndexEntry = {
6
7
  agent: string;
7
8
  name: string;
package/dist/accounts.js CHANGED
@@ -16,7 +16,7 @@ import { KEY_PREFIX, MEMBER_ID, NEW_MEMBER_ID, keyAgentId, untilListed, untilUnl
16
16
  import { routes, routeFor } from './routes.js';
17
17
  import { words } from './words.js';
18
18
  const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
19
- // `models.list { agentId, view: 'default' }` rows of the account's provider, mapped to `provider/model`. The pin
19
+ // `models.list { agentId, view: 'all' }` rows of the account's provider, mapped to `provider/model`. The pin
20
20
  // reports no per-plan availability, so `why: 'plan'` only comes from a run ending `plan`, never from here.
21
21
  export const modelsOf = (provider, answer) => {
22
22
  const rows = record(answer) && Array.isArray(answer.models) ? answer.models : [];
@@ -48,6 +48,16 @@ export const modelsOf = (provider, answer) => {
48
48
  }
49
49
  return out;
50
50
  };
51
+ // The account's models: its provider's rows, plus the Codex CLI route's rows when the account is ready. The pin reports
52
+ // those as missing-auth (no auth reaches the Codex route from the ChatGPT login), so the kit, which holds the login, is the
53
+ // signal for them.
54
+ export const modelsOfAccount = (row, answer) => {
55
+ const own = modelsOf(row.provider, answer);
56
+ if (row.provider !== 'openai' || row.state !== 'ready')
57
+ return own;
58
+ const codex = modelsOf('openai-codex', answer).map(({ why: _why, ...m }) => ({ ...m, available: true }));
59
+ return [...own, ...codex];
60
+ };
51
61
  const ACCOUNT_AGENT = /^[a-z][a-z0-9-]{0,31}--[0-9a-f]{6}$/;
52
62
  const usable = (status) => status === 'ok' || status === 'expiring' || status === 'static';
53
63
  const IDENTITY = 'IDENTITY.md';
@@ -512,7 +522,14 @@ export function createAccounts(ctx) {
512
522
  }
513
523
  return { rows, dirty };
514
524
  };
515
- const gather = (agentId) => Promise.all([rosterOf(), profilesOf(agentId)]);
525
+ // A detected Claude Code login has no profile in the kit: it stays in Claude Code. The engine's detection says whether
526
+ // it is there, and a detected one is the member's signed-in claude-cli profile for this read.
527
+ const claudeDetected = (agentId) => ctx.request('openclaw.setup.detect', { agentId }, { timeoutMs: 20_000 })
528
+ .then((r) => r.candidates?.some((c) => c.kind === 'claude-cli' && c.credentials === true) === true, () => false);
529
+ const gather = async (agentId) => {
530
+ const [roster, profiles, claude] = await Promise.all([rosterOf(), profilesOf(agentId), claudeDetected(agentId)]);
531
+ return [roster, claude && !profiles.some((p) => p.provider === 'claude-cli') ? [...profiles, { provider: 'claude-cli', status: 'ok' }] : profiles];
532
+ };
516
533
  const buildRows = async (member, agentId) => {
517
534
  const file = read(member);
518
535
  const [roster, profiles] = await gather(agentId);
@@ -638,7 +655,7 @@ export function createAccounts(ctx) {
638
655
  return readModels(row, entry);
639
656
  }
640
657
  // The shared hold covers the read, so no kit read of the agent is in flight while it is removed.
641
- const readModels = (row, entry) => ctx.locks.shared(entry.agent, async () => modelsOf(row.provider, await ctx.request('models.list', { agentId: entry.agent, view: 'default' })));
658
+ const readModels = (row, entry) => ctx.locks.shared(entry.agent, async () => modelsOfAccount(row, await ctx.request('models.list', { agentId: entry.agent, view: 'all' })));
642
659
  async function room(member, id, demand) {
643
660
  check(member);
644
661
  await ctx.ensure(member);
@@ -812,8 +829,8 @@ export function createAccounts(ctx) {
812
829
  return failed('start_failed', words('move.taken', { name: row.name }));
813
830
  // The session's model when the target lists it, else the target's first available; the read takes no shared
814
831
  // hold (this move holds the target exclusively). With no answer the fork keeps the parent's own selection.
815
- const models = await ctx.request('models.list', { agentId: entry.agent, view: 'default' })
816
- .then((answer) => modelsOf(row.provider, answer), () => []);
832
+ const models = await ctx.request('models.list', { agentId: entry.agent, view: 'all' })
833
+ .then((answer) => modelsOfAccount(row, answer), () => []);
817
834
  const current = typeof session.model !== 'string' ? undefined
818
835
  : (typeof session.modelProvider === 'string' && !session.model.includes('/') ? `${session.modelProvider}/${session.model}` : session.model).toLowerCase();
819
836
  const model = models.find((m) => m.id.toLowerCase() === current)?.id ?? models.find((m) => m.available)?.id;
package/dist/bridge.js CHANGED
@@ -3,6 +3,7 @@ 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';
6
7
  import { words } from './words.js';
7
8
  /** Newline-framed JSON, one request per connection; anything larger is not a gate request. */
8
9
  const FRAME_CAP = 1_000_000;
@@ -38,6 +39,8 @@ export function writePlugin(dir, o) {
38
39
  name: 'BYOKit bridge',
39
40
  activation: { onStartup: true },
40
41
  cliBackends: ['openai-codex'],
42
+ // The Codex CLI route lists the ChatGPT catalogue's models; the engine lists a plugin route's rows only from here.
43
+ modelCatalog: { providers: { 'openai-codex': { models: [provider('chatgpt').models.strong, provider('chatgpt').models.fast].map((id) => ({ id, name: id })) } }, discovery: { 'openai-codex': 'static' } },
41
44
  contracts: { tools: o.tools.map((t) => t.name), ...(o.browser ? { agentToolResultMiddleware: ['openclaw', 'codex'] } : {}) },
42
45
  configSchema: { type: 'object', additionalProperties: false },
43
46
  };
package/dist/device.d.ts CHANGED
@@ -2,9 +2,9 @@ import type { BrowserDevice, SignInRefusedWhy } from './browser.ts';
2
2
  import type { DeviceLink } from '@byokit/pair';
3
3
  import type { RouteView } from './routes.ts';
4
4
  import { type AccountView } from './words.ts';
5
- import type { Approval, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, KitState, RunEnd, OutputSchema, SchemaOutput, RunEvent, SignInView, AccountRef } from './types.ts';
5
+ import type { Approval, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, KitState, RunEnd, OutputSchema, SchemaOutput, RunEvent, RunFrame, SignInView, AccountRef } from './types.ts';
6
6
  export type { GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult } from './types.ts';
7
- export type { Approval, Decision, KitState, PlanWindow, Route, RunEnd, RunEvent, RunUsage, SignInView, OutputSchema, SchemaOutput } from './types.ts';
7
+ export type { Approval, Decision, KitState, PlanWindow, Route, RunEnd, RunEvent, RunFrame, RunUsage, SignInView, OutputSchema, SchemaOutput } from './types.ts';
8
8
  /** One `oc.events` frame: a member's Gateway event, typed by name, or an approval add/resolve (7.2). */
9
9
  export type OpenClawLinkEvent = {
10
10
  [E in GatewayEventName]: {
@@ -30,11 +30,13 @@ export type SessionRow = {
30
30
  /** The frame `oc.run`'s stream ends with: the run's `RunEnd`, beside its events. */
31
31
  export type DeviceEndFrame<T = unknown> = {
32
32
  type: 'end';
33
+ seq: number;
33
34
  end: RunEnd<T>;
34
35
  };
35
36
  /** `oc.run`'s options beside the message: the member's own session, account, and the kit's run options. */
36
37
  export type DeviceRunOptions<S extends OutputSchema | undefined = OutputSchema | undefined> = {
37
38
  schema?: S;
39
+ idempotencyKey?: string;
38
40
  sessionKey?: string;
39
41
  account?: AccountRef | 'default' | 'auto';
40
42
  needs?: string[];
@@ -96,6 +98,9 @@ export type OpenClawDevice = {
96
98
  signOut(p: string): Promise<void>;
97
99
  sessions(): Promise<SessionRow[]>;
98
100
  run<const S extends OutputSchema | undefined = undefined>(message: string, o?: DeviceRunOptions<S>): AsyncIterable<DeviceRunEvent | DeviceEndFrame<SchemaOutput<S>>>;
101
+ attach(runId: string, o: {
102
+ fromSeq: number;
103
+ }): AsyncIterable<RunFrame>;
99
104
  steer(k: string, t: string, o?: {
100
105
  auth?: 'apiKey';
101
106
  }): Promise<void>;
package/dist/device.js CHANGED
@@ -361,8 +361,9 @@ export function openclawDevice(link) {
361
361
  await link.request('oc.signout', { provider: p });
362
362
  },
363
363
  sessions: () => link.request('oc.sessions'),
364
+ attach: (runId, o) => liveStream(() => link.stream('oc.attach', { runId, fromSeq: o.fromSeq })),
364
365
  run: (message, o) => {
365
- const inner = liveStream(() => link.stream('oc.run', { message, ...(o?.sessionKey ? { sessionKey: checkKey(o.sessionKey) } : {}),
366
+ const inner = liveStream(() => link.stream('oc.run', { message, ...(o?.idempotencyKey !== undefined ? { idempotencyKey: o.idempotencyKey } : {}), ...(o?.sessionKey ? { sessionKey: checkKey(o.sessionKey) } : {}),
366
367
  ...(o?.account !== undefined ? { account: o.account } : {}),
367
368
  ...(o?.needs !== undefined ? { needs: o.needs } : {}),
368
369
  ...(o?.model !== undefined ? { model: o.model } : {}), ...(o?.system !== undefined ? { system: o.system } : {}),
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.ts';
2
- export type { Account, AccountId, AccountPick, AccountRef, Approval, CallOptions, ChannelChat, ChannelMessage, Considered, Decision, Defaults, ModelInfo, MoveResult, PickWhy, Room, RoomSpan, RunSelection, SignInState, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, LearningCapture, LearningMode, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, DelegatedRun, DelegatedEvent, RunSpec, OutputSchema, SchemaOutput, RunUsage, ThinkingAsked, ThinkingReport, ThinkingUsed, SignInOptions, SignInView, TelegramChannel, ToolHost, ToolSpec, } from './types.ts';
2
+ export type { Account, AccountId, AccountPick, AccountRef, Approval, CallOptions, ChannelChat, ChannelMessage, Considered, Decision, Defaults, ModelInfo, MoveResult, PickWhy, Room, RoomSpan, RunSelection, SignInState, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, LearningCapture, LearningMode, Member, PlanWindow, Route, RunEnd, RunEvent, RunFrame, RunRef, DelegatedRun, DelegatedEvent, RunSpec, OutputSchema, SchemaOutput, RunUsage, ThinkingAsked, ThinkingReport, ThinkingUsed, SignInOptions, SignInView, TelegramChannel, ToolHost, ToolSpec, } from './types.ts';
3
3
  export type { GatewayMethods } from './generated/methods.ts';
4
4
  export type { JsonValue } from './output.ts';
5
5
  export type { RouteFacts, RouteView } from './routes.ts';
package/dist/kit.d.ts CHANGED
@@ -64,6 +64,8 @@ export declare class OpenClawKit {
64
64
  private members?;
65
65
  private readonly locks;
66
66
  private readonly routeMap;
67
+ private readonly runReplay;
68
+ attach(member: Member, runId: string, fromSeq: number, signal: AbortSignal): AsyncIterable<import("./types.ts").RunFrame>;
67
69
  private installs;
68
70
  private accountsCtl?;
69
71
  private readonly bridge;
package/dist/kit.js CHANGED
@@ -18,7 +18,7 @@ import { createAccounts, memberOfAccount } from './accounts.js';
18
18
  import { createLocks } from './locks.js';
19
19
  import { keyAgentId, KEY_PREFIX, MEMBER_ID } from './members.js';
20
20
  import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from './migrate.js';
21
- import { createRuns, signedInProviders } from './runs.js';
21
+ import { createRuns, RunReplay, signedInProviders } from './runs.js';
22
22
  import { authStatus, createAuthStatus } from './auth-status.js';
23
23
  import { outputSchema } from './output.js';
24
24
  import { createKeys } from './keys.js';
@@ -442,6 +442,10 @@ export class OpenClawKit {
442
442
  members;
443
443
  locks = createLocks();
444
444
  routeMap = new Map();
445
+ runReplay = new RunReplay();
446
+ attach(member, runId, fromSeq, signal) {
447
+ return this.runReplay.attach(member, runId, fromSeq, signal);
448
+ }
445
449
  installs = Promise.resolve();
446
450
  accountsCtl;
447
451
  // One bridge and one approval surface per kit: listeners and run registrations survive reconnects (B3).
@@ -948,6 +952,8 @@ export class OpenClawKit {
948
952
  });
949
953
  }
950
954
  closed(why) {
955
+ if (why === 'engine exited')
956
+ this.runReplay.clear();
951
957
  // A first start's loop owns an engine exit until it reaches ready; other closes are the transport client's to retry.
952
958
  if (this.startRun && !this.reachedReady) {
953
959
  if (why === 'engine exited')
@@ -976,6 +982,7 @@ export class OpenClawKit {
976
982
  return delay(retryAt - Date.now(), undefined, { signal });
977
983
  }
978
984
  async stop() {
985
+ this.runReplay.clear();
979
986
  this.stopping = true;
980
987
  this.reachedReady = false;
981
988
  this.startRun?.abort();
@@ -1311,10 +1318,11 @@ export class OpenClawKit {
1311
1318
  confirmRetainedLogin(member, source) {
1312
1319
  return confirmLogin({ ...this.signInCtx(), seal: this.o.authSeal, log: this.o.log }, member, source);
1313
1320
  }
1314
- // runs (5.8): the facade owns no run state; every call delegates to the O8 module
1321
+ // runs (5.8): the facade retains the live journal; every dispatch delegates to the O8 module
1315
1322
  // with this kit's live transport, member table and bridge (spec item O9).
1316
1323
  runs() {
1317
1324
  return createRuns({
1325
+ replay: this.runReplay,
1318
1326
  request: (method, params, o) => this.request()(method, params, o),
1319
1327
  authStatus: this.admission.read,
1320
1328
  invalidateAuth: this.admission.invalidate,
package/dist/link.js CHANGED
@@ -30,8 +30,9 @@ function memberSessions(result, owns) {
30
30
  }
31
31
  /** `oc.run`'s run options, type-checked; the kit's own checks (account, tool names) still apply. */
32
32
  function runOptions(args) {
33
- const { message, model, account, needs, auth, system, images, thinking, tools, schema } = args;
33
+ const { message, idempotencyKey, model, account, needs, auth, system, images, thinking, tools, schema } = args;
34
34
  const ok = typeof message === 'string'
35
+ && (idempotencyKey === undefined || typeof idempotencyKey === 'string' && idempotencyKey.length > 0)
35
36
  && (model === undefined || typeof model === 'string')
36
37
  && (account === undefined || typeof account === 'string')
37
38
  && (needs === undefined || (Array.isArray(needs) && needs.every((n) => typeof n === 'string')))
@@ -46,6 +47,7 @@ function runOptions(args) {
46
47
  outputSchema(schema);
47
48
  return {
48
49
  message: message,
50
+ ...(idempotencyKey === undefined ? {} : { idempotencyKey: idempotencyKey }),
49
51
  ...(schema === undefined ? {} : { schema: schema }),
50
52
  ...(model === undefined ? {} : { model: model }),
51
53
  ...(account === undefined ? {} : { account: account }),
@@ -561,6 +563,31 @@ export function openclawLink(kit, o) {
561
563
  }
562
564
  return;
563
565
  }
566
+ if (req.op === 'oc.attach') {
567
+ if (grant.role === 'view')
568
+ throw refused();
569
+ const args = isRecord(req.args) ? req.args : {};
570
+ if (typeof args.runId !== 'string' || !args.runId.length || !Number.isSafeInteger(args.fromSeq) || args.fromSeq < 0)
571
+ throw refused();
572
+ const controller = new AbortController();
573
+ let frames;
574
+ try {
575
+ frames = kit.attach(member, args.runId, args.fromSeq, controller.signal);
576
+ }
577
+ catch {
578
+ throw refused();
579
+ }
580
+ s.onEnd = () => controller.abort();
581
+ try {
582
+ for await (const frame of frames)
583
+ await s.write(`${JSON.stringify(frame)}\n`);
584
+ s.end();
585
+ }
586
+ finally {
587
+ controller.abort();
588
+ }
589
+ return;
590
+ }
564
591
  if (req.op === 'oc.run') {
565
592
  if (grant.role === 'view')
566
593
  throw refused();
@@ -579,8 +606,9 @@ export function openclawLink(kit, o) {
579
606
  tail = tail.then(() => s.write(`${JSON.stringify(frame)}\n`)).catch(() => { });
580
607
  };
581
608
  try {
582
- const end = await kit.run({ member, sessionKey, ...options }, (e) => send(e));
583
- send({ type: 'end', end });
609
+ let seq = 0;
610
+ const end = await kit.run({ member, sessionKey, ...options }, (e) => { seq = e.seq; send(e); });
611
+ send({ type: 'end', seq: seq + 1, end });
584
612
  await tail;
585
613
  s.end();
586
614
  }
package/dist/runs.d.ts CHANGED
@@ -4,14 +4,33 @@ import type { createAccounts } from './accounts.ts';
4
4
  import type { createLocks } from './locks.ts';
5
5
  import { authStatus } from './auth-status.ts';
6
6
  import { outputSchema, type OutputSchema, type SchemaOutput } from './output.ts';
7
- import type { GatewayTransport, Member, RunEnd, RunEvent, RunSpec } from './types.ts';
7
+ import type { GatewayTransport, Member, RunEnd, RunEvent, RunFrame, RunSpec } from './types.ts';
8
8
  export declare const USABLE: Set<string>;
9
9
  /**
10
10
  * The providers a `models.authStatus` answer shows usable (the rule a picked account is checked by), lowercased and
11
11
  * unique; undefined while the engine has no prepared status (unknown, not none).
12
12
  */
13
13
  export declare function signedInProviders(status: unknown): string[] | undefined;
14
+ type Journal = {
15
+ member: Member;
16
+ frames: Exclude<RunFrame, {
17
+ type: 'truncated';
18
+ }>[];
19
+ done: boolean;
20
+ wake: Set<() => void>;
21
+ };
22
+ /** Live-kit only: bounded history, with readers pulling by cursor rather than growing per-reader queues. */
23
+ export declare class RunReplay {
24
+ private readonly runs;
25
+ begin(member: Member, id: string): Journal | undefined;
26
+ push(row: Journal | undefined, frame: Exclude<RunFrame, {
27
+ type: 'truncated';
28
+ }>): void;
29
+ clear(): void;
30
+ attach(member: Member, id: string, fromSeq: number, signal: AbortSignal): AsyncIterable<RunFrame>;
31
+ }
14
32
  export declare function createRuns(ctx: {
33
+ replay?: RunReplay;
15
34
  request: GatewayTransport['request'];
16
35
  authStatus?: (agentId: string, refresh: boolean, native: boolean) => ReturnType<typeof authStatus>;
17
36
  invalidateAuth?: () => void;
@@ -32,3 +51,4 @@ export declare function createRuns(ctx: {
32
51
  steer(k: string, t: string): Promise<void>;
33
52
  abort(k: string): Promise<void>;
34
53
  };
54
+ export {};
package/dist/runs.js CHANGED
@@ -71,6 +71,74 @@ function account(model) {
71
71
  throw new Error(`refused: "${model}" is not a provider/model reference`);
72
72
  return { provider, model: model.slice(model.indexOf('/') + 1) }; // the engine lowercases ids
73
73
  }
74
+ /** Live-kit only: bounded history, with readers pulling by cursor rather than growing per-reader queues. */
75
+ export class RunReplay {
76
+ runs = new Map();
77
+ begin(member, id) {
78
+ if (this.runs.has(id))
79
+ return undefined; // engine-cache retry must not replace the original stream
80
+ const row = { member, frames: [], done: false, wake: new Set() };
81
+ this.runs.set(id, row);
82
+ return row;
83
+ }
84
+ push(row, frame) {
85
+ if (!row)
86
+ return;
87
+ row.frames.push(frame);
88
+ if (row.frames.length > 32)
89
+ row.frames.shift();
90
+ row.done = frame.type === 'end';
91
+ for (const wake of row.wake)
92
+ wake();
93
+ if (row.done) {
94
+ const completed = [...this.runs].filter(([, r]) => r.done);
95
+ for (const [id] of completed.slice(0, -128))
96
+ this.runs.delete(id);
97
+ }
98
+ }
99
+ clear() {
100
+ for (const row of this.runs.values())
101
+ for (const wake of row.wake)
102
+ wake();
103
+ this.runs.clear();
104
+ }
105
+ attach(member, id, fromSeq, signal) {
106
+ const row = this.runs.get(id);
107
+ if (row && row.member !== member)
108
+ throw new Error('foreign run');
109
+ const runs = this.runs;
110
+ return (async function* () {
111
+ let cursor = fromSeq;
112
+ while (!signal.aborted) {
113
+ if (!row || runs.get(id) !== row) {
114
+ yield { type: 'truncated', reason: 'not-found' };
115
+ return;
116
+ }
117
+ const firstSeq = row.frames[0]?.seq ?? 1;
118
+ const lastSeq = row.frames.at(-1)?.seq ?? 0;
119
+ if (cursor < firstSeq - 1 || cursor > lastSeq) {
120
+ yield { type: 'truncated', reason: 'truncated', firstSeq };
121
+ return;
122
+ }
123
+ const next = row.frames.find(f => f.seq > cursor);
124
+ if (next) {
125
+ cursor = next.seq;
126
+ yield next;
127
+ if (next.type === 'end')
128
+ return;
129
+ continue;
130
+ }
131
+ if (row.done)
132
+ return;
133
+ await new Promise(resolve => {
134
+ const wake = () => { row.wake.delete(wake); signal.removeEventListener('abort', wake); resolve(); };
135
+ row.wake.add(wake);
136
+ signal.addEventListener('abort', wake, { once: true });
137
+ });
138
+ }
139
+ })();
140
+ }
141
+ }
74
142
  export function createRuns(ctx) {
75
143
  // The plan window the engine last read for the run's provider: `models.authStatus` answers from its cache without
76
144
  // blocking. Nothing reported, or no answer in time, is no window.
@@ -90,7 +158,7 @@ export function createRuns(ctx) {
90
158
  ctx.invalidateAuth?.();
91
159
  return { ok: false, ...classified, message };
92
160
  };
93
- const run = async (spec, on, keyAgent, preparedOutput) => {
161
+ const execute = async (spec, on, keyAgent, preparedOutput) => {
94
162
  // Member boundary first: a member never speaks in another member's session, refused before any request. A key
95
163
  // of one of the member's account agents is the member's too; binding below checks the index names it.
96
164
  if (!spec.sessionKey.startsWith(`agent:${spec.member}:`) && !(ctx.accounts && !keyAgent && spec.sessionKey.startsWith(`agent:${spec.member}--`)))
@@ -351,8 +419,25 @@ export function createRuns(ctx) {
351
419
  releaseDelegation?.();
352
420
  }
353
421
  };
422
+ const replay = ctx.replay ?? new RunReplay();
354
423
  return {
355
- run,
424
+ run: async (spec, on, keyAgent, preparedOutput) => {
425
+ if (spec.idempotencyKey !== undefined && (typeof spec.idempotencyKey !== 'string' || !spec.idempotencyKey.length))
426
+ throw new Error('refused: idempotencyKey must be a non-empty string');
427
+ const id = spec.idempotencyKey ?? randomUUID();
428
+ const row = replay.begin(spec.member, id);
429
+ let seq = 0;
430
+ const emit = (e) => { const event = { ...e, seq: ++seq }; replay.push(row, event); on?.(event); };
431
+ try {
432
+ const end = await execute({ ...spec, idempotencyKey: id }, emit, keyAgent, preparedOutput);
433
+ replay.push(row, { type: 'end', seq: ++seq, end });
434
+ return end;
435
+ }
436
+ catch (error) {
437
+ replay.push(row, { type: 'end', seq: ++seq, end: failure(error instanceof Error ? error.message : String(error)) });
438
+ throw error;
439
+ }
440
+ },
356
441
  steer: (k, t) => ctx.request('sessions.steer', { sessionKey: ctx.routes.get(k)?.session ?? k, message: t }),
357
442
  abort: async (k) => { await ctx.request('chat.abort', { sessionKey: ctx.routes.get(k)?.session ?? k }); },
358
443
  };
package/dist/types.d.ts CHANGED
@@ -80,6 +80,8 @@ export interface RunSpec<S extends OutputSchema | undefined = OutputSchema | und
80
80
  register?: boolean;
81
81
  }
82
82
  export type RunEvent = {
83
+ seq: number;
84
+ } & ({
83
85
  type: 'started';
84
86
  } | {
85
87
  type: 'thinking';
@@ -103,6 +105,15 @@ export type RunEvent = {
103
105
  input?: Record<string, unknown>;
104
106
  output?: unknown;
105
107
  error?: boolean;
108
+ });
109
+ export type RunFrame = RunEvent | {
110
+ type: 'end';
111
+ seq: number;
112
+ end: RunEnd;
113
+ } | {
114
+ type: 'truncated';
115
+ reason: 'truncated' | 'not-found';
116
+ firstSeq?: number;
106
117
  };
107
118
  /** Token usage of one run as the engine totals it (every model call of the run, compaction included). */
108
119
  export type RunUsage = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/openclaw",
3
- "version": "0.17.0",
3
+ "version": "0.18.3",
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.0",
63
+ "@byokit/accounts": "0.28.1",
64
64
  "@byokit/pair": "0.10.4",
65
65
  "@byokit/relay": "0.8.2",
66
66
  "@byokit/discover": "0.8.4",