@parall/parall 1.26.3 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config-manager.js +1 -1
- package/dist/fork.d.ts +1 -0
- package/dist/fork.d.ts.map +1 -1
- package/dist/fork.js +5 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +45 -4
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +41 -6
- package/dist/oc-session.d.ts +27 -0
- package/dist/oc-session.d.ts.map +1 -0
- package/dist/oc-session.js +281 -0
- package/dist/runtime.d.ts +5 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +11 -0
- package/package.json +3 -4
- package/src/config-manager.ts +1 -1
- package/src/fork.ts +6 -1
- package/src/gateway.ts +47 -3
- package/src/hooks.ts +41 -6
- package/src/oc-session.ts +337 -0
- package/src/runtime.ts +15 -0
package/dist/config-manager.js
CHANGED
|
@@ -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
|
*
|
package/dist/fork.d.ts.map
CHANGED
|
@@ -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 "
|
|
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
|
// ---------------------------------------------------------------------------
|
package/dist/gateway.d.ts.map
CHANGED
|
@@ -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;
|
|
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 "
|
|
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
|
-
|
|
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);
|
package/dist/hooks.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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 (
|
|
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;
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.28.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.
|
|
20
|
-
"@parall/sdk": "1.
|
|
19
|
+
"@parall/agent-core": "1.28.0",
|
|
20
|
+
"@parall/sdk": "1.28.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": {
|
package/src/config-manager.ts
CHANGED
|
@@ -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 "
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
}
|