@parall/parall 1.26.3 → 1.27.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.
@@ -71,7 +71,7 @@ function applyToOpenClawConfig(configPath, platformConfig, credentials) {
71
71
  // Strip keys OpenClaw doesn't recognize from agents.defaults — platform-only
72
72
  // keys like thinking_effort are consumed by agent-core's PlatformConfigManager.
73
73
  // See: OpenClaw agents.defaults config schema.
74
- const OPENCLAW_AGENTS_DEFAULTS_KEYS = new Set(["model", "compaction"]);
74
+ const OPENCLAW_AGENTS_DEFAULTS_KEYS = new Set(["model", "compaction", "memorySearch"]);
75
75
  // Unconditionally sanitize existing defaults to self-heal already-poisoned
76
76
  // openclaw.json files (e.g. mode=self agents that were poisoned while in
77
77
  // platform mode).
package/dist/fork.d.ts CHANGED
@@ -4,6 +4,7 @@ export type ForkSessionResult = {
4
4
  sessionFile: string;
5
5
  };
6
6
  export declare function resolveTranscriptFile(sessionsDir: string, sessionKey: string): string | null;
7
+ export declare function resolveSessionId(sessionsDir: string, sessionKey: string): string | null;
7
8
  /**
8
9
  * Fork the orchestrator session's transcript into a new parallel session.
9
10
  *
@@ -1 +1 @@
1
- {"version":3,"file":"fork.d.ts","sourceRoot":"","sources":["../src/fork.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AA+CF,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAqB5F;AAMD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,sBAAsB,EAAE,MAAM,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,iBAAiB,GAAG,IAAI,CA+D3B;AAMD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,IAAI,CASP"}
1
+ {"version":3,"file":"fork.d.ts","sourceRoot":"","sources":["../src/fork.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AA+CF,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAqB5F;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGvF;AAMD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,sBAAsB,EAAE,MAAM,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,iBAAiB,GAAG,IAAI,CA+D3B;AAMD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,IAAI,CASP"}
package/dist/fork.js CHANGED
@@ -9,7 +9,7 @@
9
9
  import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import * as crypto from "node:crypto";
12
- import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
12
+ import { CURRENT_SESSION_VERSION, SessionManager } from "./oc-session.js";
13
13
  function readStoreEntry(sessionsDir, sessionKey) {
14
14
  const storeFile = path.join(sessionsDir, "sessions.json");
15
15
  try {
@@ -73,6 +73,10 @@ export function resolveTranscriptFile(sessionsDir, sessionKey) {
73
73
  return null;
74
74
  }
75
75
  }
76
+ export function resolveSessionId(sessionsDir, sessionKey) {
77
+ const entry = readStoreEntry(sessionsDir, sessionKey);
78
+ return entry?.sessionId?.trim() || null;
79
+ }
76
80
  // ---------------------------------------------------------------------------
77
81
  // Fork creation
78
82
  // ---------------------------------------------------------------------------
@@ -1 +1 @@
1
- {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,uGAAuG;AACvG,KAAK,qBAAqB,CAAC,CAAC,GAAG,OAAO,IAAI,WAAW,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AAsBnF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AA6NxD,eAAO,MAAM,aAAa,EAAE,qBAAqB,CAAC,qBAAqB,CA2HtE,CAAC"}
1
+ {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,uGAAuG;AACvG,KAAK,qBAAqB,CAAC,CAAC,GAAG,OAAO,IAAI,WAAW,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AAuBnF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAkQxD,eAAO,MAAM,aAAa,EAAE,qBAAqB,CAAC,qBAAqB,CAiItE,CAAC"}
package/dist/gateway.js CHANGED
@@ -4,12 +4,12 @@ import * as crypto from "node:crypto";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { resolveParallAccount } from "./accounts.js";
7
- import { getParallRuntime, removeParallAccountState, setAgentIdentity, setDispatchGroupKey, setParallAccountState, } from "./runtime.js";
7
+ import { getParallRuntime, removeParallAccountState, setAgentIdentity, setAgentSessionBinding, setDispatchGroupKey, setParallAccountState, } from "./runtime.js";
8
8
  import { buildOrchestratorSessionKey } from "./session.js";
9
9
  import { fetchAndApplyPlatformConfig } from "./config-manager.js";
10
10
  import { startWikiHelper } from "./wiki-helper.js";
11
- import { SessionManager } from "@mariozechner/pi-coding-agent";
12
- import { cleanupForkSession, forkOrchestratorSession, resolveTranscriptFile } from "./fork.js";
11
+ import { SessionManager } from "./oc-session.js";
12
+ import { cleanupForkSession, forkOrchestratorSession, resolveSessionId, resolveTranscriptFile } from "./fork.js";
13
13
  function resolveWsUrl(account) {
14
14
  if (account.config.ws_url)
15
15
  return account.config.ws_url;
@@ -107,7 +107,9 @@ function createRuntimeEventStream() {
107
107
  },
108
108
  fail(err) {
109
109
  failure = err;
110
- flush();
110
+ while (waiters.length > 0) {
111
+ waiters.shift().reject(err);
112
+ }
111
113
  },
112
114
  async *stream() {
113
115
  while (true) {
@@ -129,6 +131,19 @@ function createRuntimeEventStream() {
129
131
  },
130
132
  };
131
133
  }
134
+ async function waitForOpenClawSessionId(sessionsDir, sessionKey, timeoutMs = 10_000) {
135
+ const existing = resolveSessionId(sessionsDir, sessionKey);
136
+ if (existing)
137
+ return existing;
138
+ const deadline = Date.now() + timeoutMs;
139
+ while (Date.now() < deadline) {
140
+ await new Promise((resolve) => setTimeout(resolve, 100));
141
+ const sessionId = resolveSessionId(sessionsDir, sessionKey);
142
+ if (sessionId)
143
+ return sessionId;
144
+ }
145
+ return null;
146
+ }
132
147
  function createOpenClawDispatchAdapter(opts) {
133
148
  return {
134
149
  async *dispatch({ event, earlierEvents = [], bodyForAgent, sessionKey }) {
@@ -172,6 +187,26 @@ function createOpenClawDispatchAdapter(opts) {
172
187
  },
173
188
  });
174
189
  run.then(() => stream.end()).catch((err) => stream.fail(err));
190
+ let sessionId;
191
+ try {
192
+ sessionId = await Promise.race([
193
+ waitForOpenClawSessionId(opts.sessionsDir, sessionKey),
194
+ run.then(() => null, (err) => { throw err; }),
195
+ ]);
196
+ }
197
+ catch {
198
+ sessionId = null;
199
+ }
200
+ if (!sessionId) {
201
+ stream.fail(new Error(`OpenClaw did not expose a sessionId for ${sessionKey}`));
202
+ }
203
+ else {
204
+ yield {
205
+ type: "runtime_session",
206
+ runtimeSessionId: sessionId,
207
+ runtimeLaneKey: sessionKey,
208
+ };
209
+ }
175
210
  yield* stream.stream();
176
211
  },
177
212
  getBranchPoint(sessionKey) {
@@ -307,11 +342,17 @@ export const parallGateway = {
307
342
  orgId: config.org_id,
308
343
  agentUserId,
309
344
  activeSessionId,
345
+ sessionBindings: new Map(),
346
+ sessionsDir,
347
+ runtimeRef: { hostname: os.hostname(), pid: process.pid },
310
348
  wikiMountRoot,
311
349
  ws,
312
350
  orchestratorSessionKey: orchestratorKey,
313
351
  });
314
352
  },
353
+ onSessionBinding: async ({ sessionKey, agentSessionId }) => {
354
+ setAgentSessionBinding(ctx.accountId, sessionKey, agentSessionId);
355
+ },
315
356
  onBeforeDisconnect: async () => {
316
357
  stopWikiHelper?.();
317
358
  removeParallAccountState(ctx.accountId);
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AA6D7D,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,iBAAiB,QAiIzD"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AA4F7D,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,iBAAiB,QAqIzD"}
package/dist/hooks.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { PRLL_BEHAVIOR, PRLL_REFERENCE_GUIDE, buildIdentity } from "@parall/agent-core";
2
2
  import { extractAccountIdFromSessionKey } from "./session.js";
3
- import { clearDispatchGroupKey, getAgentIdentity, getDispatchGroupKey, getDispatchMessageId, getParallAccountState, getSessionChatId } from "./runtime.js";
3
+ import { clearDispatchGroupKey, getAgentIdentity, getAgentSessionBinding, getDispatchGroupKey, getDispatchMessageId, getParallAccountState, getSessionChatId, setAgentSessionBinding } from "./runtime.js";
4
+ import { resolveSessionId } from "./fork.js";
4
5
  /**
5
6
  * Map the session's stored target ID (whatever the current dispatch routed
6
7
  * to — chat, task, or schedule) to the `target_type` field on agent_step.
@@ -19,7 +20,7 @@ function stepTargetType(targetId) {
19
20
  * Resolve the ParallClient + orgId + sessionId for a hook context.
20
21
  * Returns undefined if the account/session is not active.
21
22
  */
22
- function resolveClientForHook(sessionKey) {
23
+ async function resolveClientForHook(sessionKey) {
23
24
  if (!sessionKey)
24
25
  return undefined;
25
26
  const chatId = getSessionChatId(sessionKey);
@@ -31,7 +32,37 @@ function resolveClientForHook(sessionKey) {
31
32
  const state = getParallAccountState(accountId);
32
33
  if (!state)
33
34
  return undefined;
34
- return { ...state, chatId, sessionId: state.activeSessionId };
35
+ let sessionId = getAgentSessionBinding(accountId, sessionKey);
36
+ if (!sessionId && sessionKey === state.orchestratorSessionKey) {
37
+ sessionId = state.activeSessionId;
38
+ }
39
+ if (!sessionId && state.sessionsDir) {
40
+ const runtimeSessionId = resolveSessionId(state.sessionsDir, sessionKey);
41
+ if (runtimeSessionId) {
42
+ const parentSessionId = sessionKey === state.orchestratorSessionKey
43
+ ? undefined
44
+ : state.orchestratorSessionKey
45
+ ? getAgentSessionBinding(accountId, state.orchestratorSessionKey)
46
+ : undefined;
47
+ try {
48
+ const session = await state.client.createAgentSession(state.orgId, state.agentUserId, {
49
+ runtime_type: "openclaw",
50
+ runtime_key: sessionKey,
51
+ runtime_lane_key: sessionKey,
52
+ runtime_session_id: runtimeSessionId,
53
+ parent_session_id: parentSessionId,
54
+ runtime_ref: state.runtimeRef,
55
+ });
56
+ sessionId = session.id;
57
+ setAgentSessionBinding(accountId, sessionKey, session.id);
58
+ }
59
+ catch {
60
+ // Hook step creation is best-effort; the gateway binding path will log
61
+ // the dispatch-level failure if the session cannot be created.
62
+ }
63
+ }
64
+ }
65
+ return { ...state, chatId, sessionId };
35
66
  }
36
67
  const PRLL_CHANNEL_CONTEXT = `## Parall Channel — Message Delivery
37
68
 
@@ -79,7 +110,7 @@ export function registerParallHooks(api) {
79
110
  const sessionKey = ctx.sessionKey;
80
111
  // (1) Create tool_call step and await to get step ID
81
112
  let stepId;
82
- const resolved = resolveClientForHook(sessionKey);
113
+ const resolved = await resolveClientForHook(sessionKey);
83
114
  if (resolved?.sessionId && event.toolCallId) {
84
115
  const groupKey = sessionKey ? getDispatchGroupKey(sessionKey) : undefined;
85
116
  try {
@@ -122,8 +153,12 @@ export function registerParallHooks(api) {
122
153
  injectedEnv.PRLL_API_KEY = state.apiKey;
123
154
  injectedEnv.PRLL_ORG_ID = state.orgId;
124
155
  injectedEnv.PRLL_AGENT_ID = state.agentUserId;
125
- if (state.activeSessionId)
156
+ if (resolved?.sessionId) {
157
+ injectedEnv.PRLL_SESSION_ID = resolved.sessionId;
158
+ }
159
+ else if (state.activeSessionId) {
126
160
  injectedEnv.PRLL_SESSION_ID = state.activeSessionId;
161
+ }
127
162
  if (stepId)
128
163
  injectedEnv.PRLL_STEP_ID = stepId;
129
164
  if (chatId) {
@@ -161,7 +196,7 @@ export function registerParallHooks(api) {
161
196
  });
162
197
  // after_tool_call -> send tool_result step to AgentSession
163
198
  api.on("after_tool_call", async (event, ctx) => {
164
- const resolved = resolveClientForHook(ctx.sessionKey);
199
+ const resolved = await resolveClientForHook(ctx.sessionKey);
165
200
  if (!resolved?.sessionId || !event.toolCallId)
166
201
  return;
167
202
  try {
@@ -0,0 +1,27 @@
1
+ export declare const CURRENT_SESSION_VERSION = 3;
2
+ export declare class SessionManager {
3
+ private sessionId;
4
+ private sessionFile;
5
+ private readonly sessionDir;
6
+ private readonly cwd;
7
+ private fileEntries;
8
+ private byId;
9
+ private labelsById;
10
+ private labelTimestampsById;
11
+ private leafId;
12
+ private flushed;
13
+ private constructor();
14
+ private load;
15
+ private initEmpty;
16
+ private buildIndex;
17
+ private rewrite;
18
+ getLeafId(): string | null;
19
+ getCwd(): string;
20
+ getSessionDir(): string;
21
+ getSessionId(): string;
22
+ getSessionFile(): string | undefined;
23
+ private getBranch;
24
+ createBranchedSession(leafId: string): string | undefined;
25
+ static open(path: string): SessionManager;
26
+ }
27
+ //# sourceMappingURL=oc-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oc-session.d.ts","sourceRoot":"","sources":["../src/oc-session.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAkHzC,qBAAa,cAAc;IACzB,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,IAAI,CAAmC;IAC/C,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,OAAO,CAAS;IAExB,OAAO;IAeP,OAAO,CAAC,IAAI;IA+BZ,OAAO,CAAC,SAAS;IAcjB,OAAO,CAAC,UAAU;IAsBlB,OAAO,CAAC,OAAO;IAOf,SAAS,IAAI,MAAM,GAAG,IAAI;IAI1B,MAAM,IAAI,MAAM;IAIhB,aAAa,IAAI,MAAM;IAIvB,YAAY,IAAI,MAAM;IAItB,cAAc,IAAI,MAAM,GAAG,SAAS;IAMpC,OAAO,CAAC,SAAS;IAWjB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IA+DzD,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc;CAO1C"}
@@ -0,0 +1,281 @@
1
+ // Minimal subset of OpenClaw's SessionManager inlined to avoid a runtime
2
+ // dependency on @mariozechner/pi-coding-agent (14 MB + 100 MB transitive).
3
+ //
4
+ // Covers only: open → getLeafId / createBranchedSession / accessors.
5
+ // Sync check: when bumping devDependencies.openclaw, diff upstream
6
+ // pi-coding-agent/dist/core/session-manager.js for new migration steps
7
+ // or structural changes to createBranchedSession / _buildIndex.
8
+ import { randomUUID } from "node:crypto";
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
10
+ import { join, resolve } from "node:path";
11
+ export const CURRENT_SESSION_VERSION = 3;
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers
14
+ // ---------------------------------------------------------------------------
15
+ function generateId(existing) {
16
+ for (let i = 0; i < 100; i++) {
17
+ const id = randomUUID().slice(0, 8);
18
+ if (!existing.has(id))
19
+ return id;
20
+ }
21
+ return randomUUID();
22
+ }
23
+ function loadEntries(filePath) {
24
+ if (!existsSync(filePath))
25
+ return [];
26
+ const lines = readFileSync(filePath, "utf-8").trim().split("\n");
27
+ const entries = [];
28
+ for (const line of lines) {
29
+ if (!line.trim())
30
+ continue;
31
+ try {
32
+ entries.push(JSON.parse(line));
33
+ }
34
+ catch {
35
+ // skip malformed
36
+ }
37
+ }
38
+ if (entries.length === 0)
39
+ return entries;
40
+ const h = entries[0];
41
+ if (h.type !== "session" || typeof h.id !== "string")
42
+ return [];
43
+ return entries;
44
+ }
45
+ // -- Migrations (v1→v2→v3) --------------------------------------------------
46
+ function migrateV1ToV2(entries) {
47
+ const ids = new Set();
48
+ let prevId = null;
49
+ for (const entry of entries) {
50
+ if (entry.type === "session") {
51
+ entry.version = 2;
52
+ continue;
53
+ }
54
+ const e = entry;
55
+ e.id = generateId(ids);
56
+ ids.add(e.id);
57
+ e.parentId = prevId;
58
+ prevId = e.id;
59
+ if (e.type === "compaction") {
60
+ const comp = e;
61
+ if (typeof comp.firstKeptEntryIndex === "number") {
62
+ const target = entries[comp.firstKeptEntryIndex];
63
+ if (target && target.type !== "session") {
64
+ comp.firstKeptEntryId = target.id;
65
+ }
66
+ delete comp.firstKeptEntryIndex;
67
+ }
68
+ }
69
+ }
70
+ }
71
+ function migrateV2ToV3(entries) {
72
+ for (const entry of entries) {
73
+ if (entry.type === "session") {
74
+ entry.version = 3;
75
+ continue;
76
+ }
77
+ const e = entry;
78
+ if (e.type === "message" && e.message && e.message.role === "hookMessage") {
79
+ e.message.role = "custom";
80
+ }
81
+ }
82
+ }
83
+ function migrate(entries) {
84
+ const header = entries.find((e) => e.type === "session");
85
+ const version = header?.version ?? 1;
86
+ if (version >= CURRENT_SESSION_VERSION)
87
+ return false;
88
+ if (version < 2)
89
+ migrateV1ToV2(entries);
90
+ if (version < 3)
91
+ migrateV2ToV3(entries);
92
+ return true;
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // SessionManager (read-only subset + createBranchedSession)
96
+ // ---------------------------------------------------------------------------
97
+ export class SessionManager {
98
+ sessionId = "";
99
+ sessionFile;
100
+ sessionDir;
101
+ cwd;
102
+ fileEntries = [];
103
+ byId = new Map();
104
+ labelsById = new Map();
105
+ labelTimestampsById = new Map();
106
+ leafId = null;
107
+ flushed = false;
108
+ constructor(cwd, sessionDir, sessionFile) {
109
+ this.cwd = cwd;
110
+ this.sessionDir = sessionDir;
111
+ if (sessionDir && !existsSync(sessionDir)) {
112
+ mkdirSync(sessionDir, { recursive: true });
113
+ }
114
+ if (sessionFile) {
115
+ this.load(sessionFile);
116
+ }
117
+ else {
118
+ this.initEmpty();
119
+ }
120
+ }
121
+ // -- Init / load -----------------------------------------------------------
122
+ load(file) {
123
+ this.sessionFile = resolve(file);
124
+ if (!existsSync(this.sessionFile)) {
125
+ const p = this.sessionFile;
126
+ this.initEmpty();
127
+ this.sessionFile = p;
128
+ return;
129
+ }
130
+ this.fileEntries = loadEntries(this.sessionFile);
131
+ if (this.fileEntries.length === 0) {
132
+ const p = this.sessionFile;
133
+ this.initEmpty();
134
+ this.sessionFile = p;
135
+ this.rewrite();
136
+ this.flushed = true;
137
+ return;
138
+ }
139
+ const header = this.fileEntries.find((e) => e.type === "session");
140
+ if (header && header.version > CURRENT_SESSION_VERSION) {
141
+ throw new Error(`Session file ${this.sessionFile} uses version ${header.version}, ` +
142
+ `but oc-session.ts only supports up to ${CURRENT_SESSION_VERSION}. ` +
143
+ `Sync oc-session.ts with upstream pi-coding-agent (see AGENTS.md step 4).`);
144
+ }
145
+ this.sessionId = header?.id ?? randomUUID();
146
+ if (migrate(this.fileEntries))
147
+ this.rewrite();
148
+ this.buildIndex();
149
+ this.flushed = true;
150
+ }
151
+ initEmpty() {
152
+ this.sessionId = randomUUID();
153
+ const timestamp = new Date().toISOString();
154
+ this.fileEntries = [
155
+ { type: "session", version: CURRENT_SESSION_VERSION, id: this.sessionId, timestamp, cwd: this.cwd },
156
+ ];
157
+ this.byId.clear();
158
+ this.labelsById.clear();
159
+ this.leafId = null;
160
+ this.flushed = false;
161
+ const ts = timestamp.replace(/[:.]/g, "-");
162
+ this.sessionFile = join(this.sessionDir, `${ts}_${this.sessionId}.jsonl`);
163
+ }
164
+ buildIndex() {
165
+ this.byId.clear();
166
+ this.labelsById.clear();
167
+ this.labelTimestampsById.clear();
168
+ this.leafId = null;
169
+ for (const entry of this.fileEntries) {
170
+ if (entry.type === "session")
171
+ continue;
172
+ const e = entry;
173
+ this.byId.set(e.id, e);
174
+ this.leafId = e.id;
175
+ if (e.type === "label") {
176
+ if (e.label) {
177
+ this.labelsById.set(e.targetId, e.label);
178
+ this.labelTimestampsById.set(e.targetId, e.timestamp);
179
+ }
180
+ else {
181
+ this.labelsById.delete(e.targetId);
182
+ this.labelTimestampsById.delete(e.targetId);
183
+ }
184
+ }
185
+ }
186
+ }
187
+ rewrite() {
188
+ if (!this.sessionFile)
189
+ return;
190
+ writeFileSync(this.sessionFile, this.fileEntries.map((e) => JSON.stringify(e)).join("\n") + "\n");
191
+ }
192
+ // -- Public accessors ------------------------------------------------------
193
+ getLeafId() {
194
+ return this.leafId;
195
+ }
196
+ getCwd() {
197
+ return this.cwd;
198
+ }
199
+ getSessionDir() {
200
+ return this.sessionDir;
201
+ }
202
+ getSessionId() {
203
+ return this.sessionId;
204
+ }
205
+ getSessionFile() {
206
+ return this.sessionFile;
207
+ }
208
+ // -- Branching -------------------------------------------------------------
209
+ getBranch(fromId) {
210
+ const path = [];
211
+ const startId = fromId ?? this.leafId;
212
+ let current = startId ? this.byId.get(startId) : undefined;
213
+ while (current) {
214
+ path.unshift(current);
215
+ current = current.parentId ? this.byId.get(current.parentId) : undefined;
216
+ }
217
+ return path;
218
+ }
219
+ createBranchedSession(leafId) {
220
+ const branch = this.getBranch(leafId);
221
+ if (branch.length === 0)
222
+ throw new Error(`Entry ${leafId} not found`);
223
+ const pathWithoutLabels = branch.filter((e) => e.type !== "label");
224
+ const newId = randomUUID();
225
+ const timestamp = new Date().toISOString();
226
+ const ts = timestamp.replace(/[:.]/g, "-");
227
+ const newFile = join(this.sessionDir, `${ts}_${newId}.jsonl`);
228
+ const header = {
229
+ type: "session",
230
+ version: CURRENT_SESSION_VERSION,
231
+ id: newId,
232
+ timestamp,
233
+ cwd: this.cwd,
234
+ parentSession: this.sessionFile,
235
+ };
236
+ const pathEntryIds = new Set(pathWithoutLabels.map((e) => e.id));
237
+ const labelsToWrite = [];
238
+ for (const [targetId, label] of this.labelsById) {
239
+ if (pathEntryIds.has(targetId)) {
240
+ labelsToWrite.push({ targetId, label, timestamp: this.labelTimestampsById.get(targetId) });
241
+ }
242
+ }
243
+ const lastEntryId = pathWithoutLabels.at(-1)?.id ?? null;
244
+ let parentId = lastEntryId;
245
+ const labelEntries = [];
246
+ for (const { targetId, label, timestamp: labelTs } of labelsToWrite) {
247
+ const le = {
248
+ type: "label",
249
+ id: generateId(pathEntryIds),
250
+ parentId,
251
+ timestamp: labelTs ?? timestamp,
252
+ targetId,
253
+ label,
254
+ };
255
+ pathEntryIds.add(le.id);
256
+ labelEntries.push(le);
257
+ parentId = le.id;
258
+ }
259
+ this.fileEntries = [header, ...pathWithoutLabels, ...labelEntries];
260
+ this.sessionId = newId;
261
+ this.sessionFile = newFile;
262
+ this.buildIndex();
263
+ const hasAssistant = this.fileEntries.some((e) => e.type === "message" && e.message?.role === "assistant");
264
+ if (hasAssistant) {
265
+ this.rewrite();
266
+ this.flushed = true;
267
+ }
268
+ else {
269
+ this.flushed = false;
270
+ }
271
+ return newFile;
272
+ }
273
+ // -- Factory ---------------------------------------------------------------
274
+ static open(path) {
275
+ const entries = loadEntries(path);
276
+ const header = entries.find((e) => e.type === "session");
277
+ const cwd = header?.cwd ?? process.cwd();
278
+ const dir = resolve(path, "..");
279
+ return new SessionManager(cwd, dir, path);
280
+ }
281
+ }
package/dist/runtime.d.ts CHANGED
@@ -10,6 +10,9 @@ export type ParallAccountState = {
10
10
  orgId: string;
11
11
  agentUserId: string;
12
12
  activeSessionId?: string;
13
+ sessionBindings?: Map<string, string>;
14
+ sessionsDir?: string;
15
+ runtimeRef?: Record<string, unknown>;
13
16
  wikiMountRoot?: string;
14
17
  ws?: ParallWs;
15
18
  orchestratorSessionKey?: string;
@@ -20,6 +23,8 @@ export declare function getParallRuntime(): PluginRuntime;
20
23
  export declare function setParallAccountState(accountId: string, state: ParallAccountState): void;
21
24
  export declare function removeParallAccountState(accountId: string): void;
22
25
  export declare function getParallAccountState(accountId: string): ParallAccountState | undefined;
26
+ export declare function setAgentSessionBinding(accountId: string, sessionKey: string, agentSessionId: string): void;
27
+ export declare function getAgentSessionBinding(accountId: string, sessionKey: string): string | undefined;
23
28
  export declare function getAllParallAccountStates(): ReadonlyMap<string, ParallAccountState>;
24
29
  export declare function setAgentIdentity(identity: AgentIdentity): void;
25
30
  export declare function getAgentIdentity(): AgentIdentity | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAIF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,QAEnD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,CAKhD;AAKD,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,QAEjF;AAED,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,QAEzD;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAEvF;AAED,wBAAgB,yBAAyB,IAAI,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAEnF;AAID,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,QAEvD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,SAAS,CAE5D"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAIF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,QAEnD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,CAKhD;AAKD,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,QAEjF;AAED,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,QAEzD;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAEvF;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,QAMnG;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEhG;AAED,wBAAgB,yBAAyB,IAAI,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAEnF;AAID,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,QAEvD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,SAAS,CAE5D"}
package/dist/runtime.js CHANGED
@@ -20,6 +20,17 @@ export function removeParallAccountState(accountId) {
20
20
  export function getParallAccountState(accountId) {
21
21
  return accountStates.get(accountId);
22
22
  }
23
+ export function setAgentSessionBinding(accountId, sessionKey, agentSessionId) {
24
+ const state = accountStates.get(accountId);
25
+ if (!state)
26
+ return;
27
+ const sessionBindings = state.sessionBindings ?? new Map();
28
+ sessionBindings.set(sessionKey, agentSessionId);
29
+ accountStates.set(accountId, { ...state, sessionBindings });
30
+ }
31
+ export function getAgentSessionBinding(accountId, sessionKey) {
32
+ return accountStates.get(accountId)?.sessionBindings?.get(sessionKey);
33
+ }
23
34
  export function getAllParallAccountStates() {
24
35
  return new Map(accountStates);
25
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/parall",
3
- "version": "1.26.3",
3
+ "version": "1.27.0",
4
4
  "description": "OpenClaw channel plugin for Parall IM",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,8 +16,8 @@
16
16
  "openclaw.plugin.json"
17
17
  ],
18
18
  "dependencies": {
19
- "@parall/agent-core": "1.26.3",
20
- "@parall/sdk": "1.26.3"
19
+ "@parall/sdk": "1.27.0",
20
+ "@parall/agent-core": "1.27.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^22.0.0",
@@ -25,7 +25,6 @@
25
25
  "typescript": "^5.7.0"
26
26
  },
27
27
  "peerDependencies": {
28
- "@mariozechner/pi-coding-agent": "*",
29
28
  "openclaw": ">=2026.5.7"
30
29
  },
31
30
  "openclaw": {
@@ -99,7 +99,7 @@ function applyToOpenClawConfig(
99
99
  // Strip keys OpenClaw doesn't recognize from agents.defaults — platform-only
100
100
  // keys like thinking_effort are consumed by agent-core's PlatformConfigManager.
101
101
  // See: OpenClaw agents.defaults config schema.
102
- const OPENCLAW_AGENTS_DEFAULTS_KEYS = new Set(["model", "compaction"]);
102
+ const OPENCLAW_AGENTS_DEFAULTS_KEYS = new Set(["model", "compaction", "memorySearch"]);
103
103
 
104
104
  // Unconditionally sanitize existing defaults to self-heal already-poisoned
105
105
  // openclaw.json files (e.g. mode=self agents that were poisoned while in
package/src/fork.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  import * as fs from "node:fs";
11
11
  import * as path from "node:path";
12
12
  import * as crypto from "node:crypto";
13
- import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
13
+ import { CURRENT_SESSION_VERSION, SessionManager } from "./oc-session.js";
14
14
 
15
15
  export type ForkSessionResult = {
16
16
  sessionKey: string;
@@ -86,6 +86,11 @@ export function resolveTranscriptFile(sessionsDir: string, sessionKey: string):
86
86
  }
87
87
  }
88
88
 
89
+ export function resolveSessionId(sessionsDir: string, sessionKey: string): string | null {
90
+ const entry = readStoreEntry(sessionsDir, sessionKey);
91
+ return entry?.sessionId?.trim() || null;
92
+ }
93
+
89
94
  // ---------------------------------------------------------------------------
90
95
  // Fork creation
91
96
  // ---------------------------------------------------------------------------
package/src/gateway.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  getParallRuntime,
21
21
  removeParallAccountState,
22
22
  setAgentIdentity,
23
+ setAgentSessionBinding,
23
24
  setDispatchGroupKey,
24
25
  setParallAccountState,
25
26
  } from "./runtime.js";
@@ -27,8 +28,8 @@ import { buildOrchestratorSessionKey } from "./session.js";
27
28
  import type { ResolvedParallAccount } from "./types.js";
28
29
  import { fetchAndApplyPlatformConfig } from "./config-manager.js";
29
30
  import { startWikiHelper } from "./wiki-helper.js";
30
- import { SessionManager } from "@mariozechner/pi-coding-agent";
31
- import { cleanupForkSession, forkOrchestratorSession, resolveTranscriptFile } from "./fork.js";
31
+ import { SessionManager } from "./oc-session.js";
32
+ import { cleanupForkSession, forkOrchestratorSession, resolveSessionId, resolveTranscriptFile } from "./fork.js";
32
33
 
33
34
  function resolveWsUrl(account: ResolvedParallAccount): string {
34
35
  if (account.config.ws_url) return account.config.ws_url;
@@ -139,7 +140,9 @@ function createRuntimeEventStream() {
139
140
  },
140
141
  fail(err: unknown) {
141
142
  failure = err;
142
- flush();
143
+ while (waiters.length > 0) {
144
+ waiters.shift()!.reject(err);
145
+ }
143
146
  },
144
147
  async *stream(): AsyncGenerator<RuntimeEvent> {
145
148
  while (true) {
@@ -160,6 +163,23 @@ function createRuntimeEventStream() {
160
163
  };
161
164
  }
162
165
 
166
+ async function waitForOpenClawSessionId(
167
+ sessionsDir: string,
168
+ sessionKey: string,
169
+ timeoutMs = 10_000,
170
+ ): Promise<string | null> {
171
+ const existing = resolveSessionId(sessionsDir, sessionKey);
172
+ if (existing) return existing;
173
+
174
+ const deadline = Date.now() + timeoutMs;
175
+ while (Date.now() < deadline) {
176
+ await new Promise((resolve) => setTimeout(resolve, 100));
177
+ const sessionId = resolveSessionId(sessionsDir, sessionKey);
178
+ if (sessionId) return sessionId;
179
+ }
180
+ return null;
181
+ }
182
+
163
183
  function createOpenClawDispatchAdapter(opts: {
164
184
  core: PluginRuntime;
165
185
  cfg: Record<string, unknown>;
@@ -208,6 +228,24 @@ function createOpenClawDispatchAdapter(opts: {
208
228
  });
209
229
 
210
230
  run.then(() => stream.end()).catch((err) => stream.fail(err));
231
+ let sessionId: string | null;
232
+ try {
233
+ sessionId = await Promise.race([
234
+ waitForOpenClawSessionId(opts.sessionsDir, sessionKey),
235
+ run.then(() => null as string | null, (err) => { throw err; }),
236
+ ]);
237
+ } catch {
238
+ sessionId = null;
239
+ }
240
+ if (!sessionId) {
241
+ stream.fail(new Error(`OpenClaw did not expose a sessionId for ${sessionKey}`));
242
+ } else {
243
+ yield {
244
+ type: "runtime_session",
245
+ runtimeSessionId: sessionId,
246
+ runtimeLaneKey: sessionKey,
247
+ };
248
+ }
211
249
  yield* stream.stream();
212
250
  },
213
251
 
@@ -348,11 +386,17 @@ export const parallGateway: ChannelGatewayAdapter<ResolvedParallAccount> = {
348
386
  orgId: config.org_id,
349
387
  agentUserId,
350
388
  activeSessionId,
389
+ sessionBindings: new Map<string, string>(),
390
+ sessionsDir,
391
+ runtimeRef: { hostname: os.hostname(), pid: process.pid },
351
392
  wikiMountRoot,
352
393
  ws,
353
394
  orchestratorSessionKey: orchestratorKey,
354
395
  });
355
396
  },
397
+ onSessionBinding: async ({ sessionKey, agentSessionId }) => {
398
+ setAgentSessionBinding(ctx.accountId, sessionKey, agentSessionId);
399
+ },
356
400
  onBeforeDisconnect: async () => {
357
401
  stopWikiHelper?.();
358
402
  removeParallAccountState(ctx.accountId);
package/src/hooks.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  import { PRLL_BEHAVIOR, PRLL_REFERENCE_GUIDE, buildIdentity } from "@parall/agent-core";
3
3
  import { extractAccountIdFromSessionKey } from "./session.js";
4
- import { clearDispatchGroupKey, getAgentIdentity, getDispatchGroupKey, getDispatchMessageId, getParallAccountState, getSessionChatId } from "./runtime.js";
4
+ import { clearDispatchGroupKey, getAgentIdentity, getAgentSessionBinding, getDispatchGroupKey, getDispatchMessageId, getParallAccountState, getSessionChatId, setAgentSessionBinding } from "./runtime.js";
5
+ import { resolveSessionId } from "./fork.js";
5
6
 
6
7
  /**
7
8
  * Map the session's stored target ID (whatever the current dispatch routed
@@ -19,7 +20,7 @@ function stepTargetType(targetId: string): string {
19
20
  * Resolve the ParallClient + orgId + sessionId for a hook context.
20
21
  * Returns undefined if the account/session is not active.
21
22
  */
22
- function resolveClientForHook(sessionKey: string | undefined) {
23
+ async function resolveClientForHook(sessionKey: string | undefined) {
23
24
  if (!sessionKey) return undefined;
24
25
  const chatId = getSessionChatId(sessionKey);
25
26
  if (!chatId) return undefined;
@@ -29,7 +30,37 @@ function resolveClientForHook(sessionKey: string | undefined) {
29
30
  const state = getParallAccountState(accountId);
30
31
  if (!state) return undefined;
31
32
 
32
- return { ...state, chatId, sessionId: state.activeSessionId };
33
+ let sessionId = getAgentSessionBinding(accountId, sessionKey);
34
+ if (!sessionId && sessionKey === state.orchestratorSessionKey) {
35
+ sessionId = state.activeSessionId;
36
+ }
37
+ if (!sessionId && state.sessionsDir) {
38
+ const runtimeSessionId = resolveSessionId(state.sessionsDir, sessionKey);
39
+ if (runtimeSessionId) {
40
+ const parentSessionId = sessionKey === state.orchestratorSessionKey
41
+ ? undefined
42
+ : state.orchestratorSessionKey
43
+ ? getAgentSessionBinding(accountId, state.orchestratorSessionKey)
44
+ : undefined;
45
+ try {
46
+ const session = await state.client.createAgentSession(state.orgId, state.agentUserId, {
47
+ runtime_type: "openclaw",
48
+ runtime_key: sessionKey,
49
+ runtime_lane_key: sessionKey,
50
+ runtime_session_id: runtimeSessionId,
51
+ parent_session_id: parentSessionId,
52
+ runtime_ref: state.runtimeRef,
53
+ });
54
+ sessionId = session.id;
55
+ setAgentSessionBinding(accountId, sessionKey, session.id);
56
+ } catch {
57
+ // Hook step creation is best-effort; the gateway binding path will log
58
+ // the dispatch-level failure if the session cannot be created.
59
+ }
60
+ }
61
+ }
62
+
63
+ return { ...state, chatId, sessionId };
33
64
  }
34
65
 
35
66
  const PRLL_CHANNEL_CONTEXT = `## Parall Channel — Message Delivery
@@ -82,7 +113,7 @@ export function registerParallHooks(api: OpenClawPluginApi) {
82
113
 
83
114
  // (1) Create tool_call step and await to get step ID
84
115
  let stepId: string | undefined;
85
- const resolved = resolveClientForHook(sessionKey);
116
+ const resolved = await resolveClientForHook(sessionKey);
86
117
  if (resolved?.sessionId && event.toolCallId) {
87
118
  const groupKey = sessionKey ? getDispatchGroupKey(sessionKey) : undefined;
88
119
  try {
@@ -122,7 +153,11 @@ export function registerParallHooks(api: OpenClawPluginApi) {
122
153
  injectedEnv.PRLL_API_KEY = state.apiKey;
123
154
  injectedEnv.PRLL_ORG_ID = state.orgId;
124
155
  injectedEnv.PRLL_AGENT_ID = state.agentUserId;
125
- if (state.activeSessionId) injectedEnv.PRLL_SESSION_ID = state.activeSessionId;
156
+ if (resolved?.sessionId) {
157
+ injectedEnv.PRLL_SESSION_ID = resolved.sessionId;
158
+ } else if (state.activeSessionId) {
159
+ injectedEnv.PRLL_SESSION_ID = state.activeSessionId;
160
+ }
126
161
  if (stepId) injectedEnv.PRLL_STEP_ID = stepId;
127
162
  if (chatId) {
128
163
  if (chatId.startsWith("sch_")) {
@@ -159,7 +194,7 @@ export function registerParallHooks(api: OpenClawPluginApi) {
159
194
 
160
195
  // after_tool_call -> send tool_result step to AgentSession
161
196
  api.on("after_tool_call", async (event, ctx) => {
162
- const resolved = resolveClientForHook(ctx.sessionKey);
197
+ const resolved = await resolveClientForHook(ctx.sessionKey);
163
198
  if (!resolved?.sessionId || !event.toolCallId) return;
164
199
  try {
165
200
  await resolved.client.createAgentStep(resolved.orgId, resolved.agentUserId, resolved.sessionId, {
@@ -0,0 +1,337 @@
1
+ // Minimal subset of OpenClaw's SessionManager inlined to avoid a runtime
2
+ // dependency on @mariozechner/pi-coding-agent (14 MB + 100 MB transitive).
3
+ //
4
+ // Covers only: open → getLeafId / createBranchedSession / accessors.
5
+ // Sync check: when bumping devDependencies.openclaw, diff upstream
6
+ // pi-coding-agent/dist/core/session-manager.js for new migration steps
7
+ // or structural changes to createBranchedSession / _buildIndex.
8
+
9
+ import { randomUUID } from "node:crypto";
10
+ import {
11
+ existsSync,
12
+ mkdirSync,
13
+ readFileSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import { join, resolve } from "node:path";
17
+
18
+ export const CURRENT_SESSION_VERSION = 3;
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Types (minimal)
22
+ // ---------------------------------------------------------------------------
23
+
24
+ interface SessionHeader {
25
+ type: "session";
26
+ version: number;
27
+ id: string;
28
+ timestamp: string;
29
+ cwd?: string;
30
+ parentSession?: string;
31
+ }
32
+
33
+ interface SessionEntry {
34
+ type: string;
35
+ id: string;
36
+ parentId: string | null;
37
+ timestamp: string;
38
+ // label entries
39
+ targetId?: string;
40
+ label?: string;
41
+ // message entries
42
+ message?: { role: string };
43
+ [key: string]: unknown;
44
+ }
45
+
46
+ type AnyEntry = SessionHeader | SessionEntry;
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Helpers
50
+ // ---------------------------------------------------------------------------
51
+
52
+ function generateId(existing: Map<string, unknown> | Set<string>): string {
53
+ for (let i = 0; i < 100; i++) {
54
+ const id = randomUUID().slice(0, 8);
55
+ if (!existing.has(id)) return id;
56
+ }
57
+ return randomUUID();
58
+ }
59
+
60
+ function loadEntries(filePath: string): AnyEntry[] {
61
+ if (!existsSync(filePath)) return [];
62
+ const lines = readFileSync(filePath, "utf-8").trim().split("\n");
63
+ const entries: AnyEntry[] = [];
64
+ for (const line of lines) {
65
+ if (!line.trim()) continue;
66
+ try {
67
+ entries.push(JSON.parse(line));
68
+ } catch {
69
+ // skip malformed
70
+ }
71
+ }
72
+ if (entries.length === 0) return entries;
73
+ const h = entries[0] as Record<string, unknown>;
74
+ if (h.type !== "session" || typeof h.id !== "string") return [];
75
+ return entries;
76
+ }
77
+
78
+ // -- Migrations (v1→v2→v3) --------------------------------------------------
79
+
80
+ function migrateV1ToV2(entries: AnyEntry[]): void {
81
+ const ids = new Set<string>();
82
+ let prevId: string | null = null;
83
+ for (const entry of entries) {
84
+ if ((entry as SessionHeader).type === "session") {
85
+ (entry as SessionHeader).version = 2;
86
+ continue;
87
+ }
88
+ const e = entry as SessionEntry;
89
+ e.id = generateId(ids);
90
+ ids.add(e.id);
91
+ e.parentId = prevId;
92
+ prevId = e.id;
93
+ if (e.type === "compaction") {
94
+ const comp = e as Record<string, unknown>;
95
+ if (typeof comp.firstKeptEntryIndex === "number") {
96
+ const target = entries[comp.firstKeptEntryIndex as number] as SessionEntry | undefined;
97
+ if (target && target.type !== "session") {
98
+ comp.firstKeptEntryId = target.id;
99
+ }
100
+ delete comp.firstKeptEntryIndex;
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ function migrateV2ToV3(entries: AnyEntry[]): void {
107
+ for (const entry of entries) {
108
+ if ((entry as SessionHeader).type === "session") {
109
+ (entry as SessionHeader).version = 3;
110
+ continue;
111
+ }
112
+ const e = entry as SessionEntry;
113
+ if (e.type === "message" && e.message && (e.message as Record<string, unknown>).role === "hookMessage") {
114
+ (e.message as Record<string, unknown>).role = "custom";
115
+ }
116
+ }
117
+ }
118
+
119
+ function migrate(entries: AnyEntry[]): boolean {
120
+ const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
121
+ const version = header?.version ?? 1;
122
+ if (version >= CURRENT_SESSION_VERSION) return false;
123
+ if (version < 2) migrateV1ToV2(entries);
124
+ if (version < 3) migrateV2ToV3(entries);
125
+ return true;
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // SessionManager (read-only subset + createBranchedSession)
130
+ // ---------------------------------------------------------------------------
131
+
132
+ export class SessionManager {
133
+ private sessionId = "";
134
+ private sessionFile: string | undefined;
135
+ private readonly sessionDir: string;
136
+ private readonly cwd: string;
137
+
138
+ private fileEntries: AnyEntry[] = [];
139
+ private byId = new Map<string, SessionEntry>();
140
+ private labelsById = new Map<string, string>();
141
+ private labelTimestampsById = new Map<string, string>();
142
+ private leafId: string | null = null;
143
+ private flushed = false;
144
+
145
+ private constructor(cwd: string, sessionDir: string, sessionFile?: string) {
146
+ this.cwd = cwd;
147
+ this.sessionDir = sessionDir;
148
+ if (sessionDir && !existsSync(sessionDir)) {
149
+ mkdirSync(sessionDir, { recursive: true });
150
+ }
151
+ if (sessionFile) {
152
+ this.load(sessionFile);
153
+ } else {
154
+ this.initEmpty();
155
+ }
156
+ }
157
+
158
+ // -- Init / load -----------------------------------------------------------
159
+
160
+ private load(file: string): void {
161
+ this.sessionFile = resolve(file);
162
+ if (!existsSync(this.sessionFile)) {
163
+ const p = this.sessionFile;
164
+ this.initEmpty();
165
+ this.sessionFile = p;
166
+ return;
167
+ }
168
+ this.fileEntries = loadEntries(this.sessionFile);
169
+ if (this.fileEntries.length === 0) {
170
+ const p = this.sessionFile;
171
+ this.initEmpty();
172
+ this.sessionFile = p;
173
+ this.rewrite();
174
+ this.flushed = true;
175
+ return;
176
+ }
177
+ const header = this.fileEntries.find((e) => e.type === "session") as SessionHeader | undefined;
178
+ if (header && header.version > CURRENT_SESSION_VERSION) {
179
+ throw new Error(
180
+ `Session file ${this.sessionFile} uses version ${header.version}, ` +
181
+ `but oc-session.ts only supports up to ${CURRENT_SESSION_VERSION}. ` +
182
+ `Sync oc-session.ts with upstream pi-coding-agent (see AGENTS.md step 4).`,
183
+ );
184
+ }
185
+ this.sessionId = header?.id ?? randomUUID();
186
+ if (migrate(this.fileEntries)) this.rewrite();
187
+ this.buildIndex();
188
+ this.flushed = true;
189
+ }
190
+
191
+ private initEmpty(): void {
192
+ this.sessionId = randomUUID();
193
+ const timestamp = new Date().toISOString();
194
+ this.fileEntries = [
195
+ { type: "session", version: CURRENT_SESSION_VERSION, id: this.sessionId, timestamp, cwd: this.cwd },
196
+ ];
197
+ this.byId.clear();
198
+ this.labelsById.clear();
199
+ this.leafId = null;
200
+ this.flushed = false;
201
+ const ts = timestamp.replace(/[:.]/g, "-");
202
+ this.sessionFile = join(this.sessionDir, `${ts}_${this.sessionId}.jsonl`);
203
+ }
204
+
205
+ private buildIndex(): void {
206
+ this.byId.clear();
207
+ this.labelsById.clear();
208
+ this.labelTimestampsById.clear();
209
+ this.leafId = null;
210
+ for (const entry of this.fileEntries) {
211
+ if (entry.type === "session") continue;
212
+ const e = entry as SessionEntry;
213
+ this.byId.set(e.id, e);
214
+ this.leafId = e.id;
215
+ if (e.type === "label") {
216
+ if (e.label) {
217
+ this.labelsById.set(e.targetId!, e.label);
218
+ this.labelTimestampsById.set(e.targetId!, e.timestamp);
219
+ } else {
220
+ this.labelsById.delete(e.targetId!);
221
+ this.labelTimestampsById.delete(e.targetId!);
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ private rewrite(): void {
228
+ if (!this.sessionFile) return;
229
+ writeFileSync(this.sessionFile, this.fileEntries.map((e) => JSON.stringify(e)).join("\n") + "\n");
230
+ }
231
+
232
+ // -- Public accessors ------------------------------------------------------
233
+
234
+ getLeafId(): string | null {
235
+ return this.leafId;
236
+ }
237
+
238
+ getCwd(): string {
239
+ return this.cwd;
240
+ }
241
+
242
+ getSessionDir(): string {
243
+ return this.sessionDir;
244
+ }
245
+
246
+ getSessionId(): string {
247
+ return this.sessionId;
248
+ }
249
+
250
+ getSessionFile(): string | undefined {
251
+ return this.sessionFile;
252
+ }
253
+
254
+ // -- Branching -------------------------------------------------------------
255
+
256
+ private getBranch(fromId?: string | null): SessionEntry[] {
257
+ const path: SessionEntry[] = [];
258
+ const startId = fromId ?? this.leafId;
259
+ let current = startId ? this.byId.get(startId) : undefined;
260
+ while (current) {
261
+ path.unshift(current);
262
+ current = current.parentId ? this.byId.get(current.parentId) : undefined;
263
+ }
264
+ return path;
265
+ }
266
+
267
+ createBranchedSession(leafId: string): string | undefined {
268
+ const branch = this.getBranch(leafId);
269
+ if (branch.length === 0) throw new Error(`Entry ${leafId} not found`);
270
+
271
+ const pathWithoutLabels = branch.filter((e) => e.type !== "label");
272
+ const newId = randomUUID();
273
+ const timestamp = new Date().toISOString();
274
+ const ts = timestamp.replace(/[:.]/g, "-");
275
+ const newFile = join(this.sessionDir, `${ts}_${newId}.jsonl`);
276
+
277
+ const header: SessionHeader = {
278
+ type: "session",
279
+ version: CURRENT_SESSION_VERSION,
280
+ id: newId,
281
+ timestamp,
282
+ cwd: this.cwd,
283
+ parentSession: this.sessionFile,
284
+ };
285
+
286
+ const pathEntryIds = new Set(pathWithoutLabels.map((e) => e.id));
287
+ const labelsToWrite: { targetId: string; label: string; timestamp?: string }[] = [];
288
+ for (const [targetId, label] of this.labelsById) {
289
+ if (pathEntryIds.has(targetId)) {
290
+ labelsToWrite.push({ targetId, label, timestamp: this.labelTimestampsById.get(targetId) });
291
+ }
292
+ }
293
+
294
+ const lastEntryId = pathWithoutLabels.at(-1)?.id ?? null;
295
+ let parentId: string | null = lastEntryId;
296
+ const labelEntries: SessionEntry[] = [];
297
+ for (const { targetId, label, timestamp: labelTs } of labelsToWrite) {
298
+ const le: SessionEntry = {
299
+ type: "label",
300
+ id: generateId(pathEntryIds),
301
+ parentId,
302
+ timestamp: labelTs ?? timestamp,
303
+ targetId,
304
+ label,
305
+ };
306
+ pathEntryIds.add(le.id);
307
+ labelEntries.push(le);
308
+ parentId = le.id;
309
+ }
310
+
311
+ this.fileEntries = [header as AnyEntry, ...pathWithoutLabels, ...labelEntries];
312
+ this.sessionId = newId;
313
+ this.sessionFile = newFile;
314
+ this.buildIndex();
315
+
316
+ const hasAssistant = this.fileEntries.some(
317
+ (e) => e.type === "message" && (e as SessionEntry).message?.role === "assistant",
318
+ );
319
+ if (hasAssistant) {
320
+ this.rewrite();
321
+ this.flushed = true;
322
+ } else {
323
+ this.flushed = false;
324
+ }
325
+ return newFile;
326
+ }
327
+
328
+ // -- Factory ---------------------------------------------------------------
329
+
330
+ static open(path: string): SessionManager {
331
+ const entries = loadEntries(path);
332
+ const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
333
+ const cwd = header?.cwd ?? process.cwd();
334
+ const dir = resolve(path, "..");
335
+ return new SessionManager(cwd, dir, path);
336
+ }
337
+ }
package/src/runtime.ts CHANGED
@@ -26,6 +26,9 @@ export type ParallAccountState = {
26
26
  orgId: string;
27
27
  agentUserId: string;
28
28
  activeSessionId?: string;
29
+ sessionBindings?: Map<string, string>;
30
+ sessionsDir?: string;
31
+ runtimeRef?: Record<string, unknown>;
29
32
  wikiMountRoot?: string;
30
33
  ws?: ParallWs;
31
34
  orchestratorSessionKey?: string;
@@ -60,6 +63,18 @@ export function getParallAccountState(accountId: string): ParallAccountState | u
60
63
  return accountStates.get(accountId);
61
64
  }
62
65
 
66
+ export function setAgentSessionBinding(accountId: string, sessionKey: string, agentSessionId: string) {
67
+ const state = accountStates.get(accountId);
68
+ if (!state) return;
69
+ const sessionBindings = state.sessionBindings ?? new Map<string, string>();
70
+ sessionBindings.set(sessionKey, agentSessionId);
71
+ accountStates.set(accountId, { ...state, sessionBindings });
72
+ }
73
+
74
+ export function getAgentSessionBinding(accountId: string, sessionKey: string): string | undefined {
75
+ return accountStates.get(accountId)?.sessionBindings?.get(sessionKey);
76
+ }
77
+
63
78
  export function getAllParallAccountStates(): ReadonlyMap<string, ParallAccountState> {
64
79
  return new Map(accountStates);
65
80
  }