@parall/parall 1.30.0 → 1.32.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.
Files changed (47) hide show
  1. package/dist/accounts.d.ts +1 -1
  2. package/dist/accounts.js +4 -4
  3. package/dist/channel.d.ts +2 -2
  4. package/dist/channel.d.ts.map +1 -1
  5. package/dist/channel.js +10 -10
  6. package/dist/config-manager.d.ts +1 -1
  7. package/dist/config-manager.d.ts.map +1 -1
  8. package/dist/config-manager.js +70 -30
  9. package/dist/fork.d.ts.map +1 -1
  10. package/dist/fork.js +17 -17
  11. package/dist/gateway.d.ts +3 -3
  12. package/dist/gateway.d.ts.map +1 -1
  13. package/dist/gateway.js +214 -171
  14. package/dist/hooks.d.ts +1 -1
  15. package/dist/hooks.d.ts.map +1 -1
  16. package/dist/hooks.js +52 -26
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +7 -7
  19. package/dist/oc-session.d.ts.map +1 -1
  20. package/dist/oc-session.js +35 -27
  21. package/dist/outbound.d.ts +2 -2
  22. package/dist/outbound.d.ts.map +1 -1
  23. package/dist/outbound.js +13 -14
  24. package/dist/routing.d.ts +2 -2
  25. package/dist/routing.js +1 -1
  26. package/dist/runtime.d.ts +5 -5
  27. package/dist/runtime.d.ts.map +1 -1
  28. package/dist/runtime.js +2 -2
  29. package/dist/session.js +4 -4
  30. package/dist/wiki-helper.d.ts.map +1 -1
  31. package/dist/wiki-helper.js +27 -27
  32. package/openclaw.plugin.json +4 -1
  33. package/package.json +3 -3
  34. package/skills/parall-clips/SKILL.md +48 -0
  35. package/src/accounts.ts +5 -5
  36. package/src/channel.ts +15 -14
  37. package/src/config-manager.ts +79 -34
  38. package/src/fork.ts +26 -22
  39. package/src/gateway.ts +245 -175
  40. package/src/hooks.ts +109 -50
  41. package/src/index.ts +8 -8
  42. package/src/oc-session.ts +43 -35
  43. package/src/outbound.ts +18 -17
  44. package/src/routing.ts +2 -2
  45. package/src/runtime.ts +11 -7
  46. package/src/session.ts +4 -4
  47. package/src/wiki-helper.ts +47 -34
@@ -1,7 +1,7 @@
1
- import type { ParallClient } from "@parall/sdk";
2
- import type { PlatformConfigResponse } from "@parall/sdk";
3
- import * as fs from "node:fs";
4
- import * as path from "node:path";
1
+ import type { ParallClient } from '@parall/sdk';
2
+ import type { PlatformConfigResponse } from '@parall/sdk';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
5
 
6
6
  interface CachedPlatformConfig {
7
7
  version: string;
@@ -17,7 +17,7 @@ interface ConfigManagerOpts {
17
17
  log?: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void };
18
18
  }
19
19
 
20
- const CACHE_FILENAME = "parall-platform-config.json";
20
+ const CACHE_FILENAME = 'parall-platform-config.json';
21
21
 
22
22
  /** Tool names are no longer registered — all operations go through CLI. */
23
23
 
@@ -27,7 +27,7 @@ function cachePath(stateDir: string): string {
27
27
 
28
28
  function loadCachedConfig(stateDir: string): CachedPlatformConfig | null {
29
29
  try {
30
- const raw = fs.readFileSync(cachePath(stateDir), "utf-8");
30
+ const raw = fs.readFileSync(cachePath(stateDir), 'utf-8');
31
31
  return JSON.parse(raw) as CachedPlatformConfig;
32
32
  } catch {
33
33
  return null;
@@ -43,13 +43,14 @@ function saveCachedConfig(stateDir: string, config: PlatformConfigResponse): voi
43
43
  const filePath = cachePath(stateDir);
44
44
  const tmpPath = `${filePath}.tmp`;
45
45
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
46
- fs.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), "utf-8");
46
+ fs.writeFileSync(tmpPath, JSON.stringify(cached, null, 2), 'utf-8');
47
47
  fs.renameSync(tmpPath, filePath);
48
48
  }
49
49
 
50
50
  /**
51
51
  * Deep-merge platform config into an existing openclaw.json file.
52
- * Only overwrites `models.providers.parall` and `agents.defaults` sections;
52
+ * Only overwrites `models.providers.parall`, `models.providers["parall-anthropic"]`,
53
+ * and `agents.defaults` sections;
53
54
  * all other keys (user customizations, other providers) are preserved.
54
55
  */
55
56
  function applyToOpenClawConfig(
@@ -59,47 +60,74 @@ function applyToOpenClawConfig(
59
60
  ): void {
60
61
  let existing: Record<string, unknown> = {};
61
62
  try {
62
- const raw = fs.readFileSync(configPath, "utf-8");
63
+ const raw = fs.readFileSync(configPath, 'utf-8');
63
64
  existing = JSON.parse(raw) as Record<string, unknown>;
64
65
  } catch {
65
66
  // File doesn't exist or is invalid — start fresh
66
67
  }
67
68
 
68
- // Deep-merge models.providers.parall (preserve existing fields, overlay platform, inject credentials)
69
+ // Deep-merge models.providers for all Parall-managed providers
70
+ // (preserve existing fields, overlay platform, inject credentials).
69
71
  const models = (existing.models ?? {}) as Record<string, unknown>;
70
72
  const providers = (models.providers ?? {}) as Record<string, unknown>;
71
- const existingParall = (providers.parall ?? {}) as Record<string, unknown>;
72
73
  const platformModels = (platformConfig.models ?? {}) as Record<string, unknown>;
73
74
  const platformProviders = (platformModels.providers ?? {}) as Record<string, unknown>;
74
- const platformParall = (platformProviders.parall ?? {}) as Record<string, unknown>;
75
75
 
76
76
  // Only forward keys OpenClaw's model schema recognizes — its strict Zod
77
77
  // validation rejects unrecognized keys and skips the entire config reload.
78
- const OPENCLAW_MODEL_KEYS = new Set(["id", "name", "contextWindow", "maxTokens"]);
79
- if (Array.isArray(platformParall.models)) {
80
- platformParall.models = (platformParall.models as Record<string, unknown>[]).map((m) => {
81
- const cleaned: Record<string, unknown> = {};
82
- for (const [k, v] of Object.entries(m)) {
83
- if (OPENCLAW_MODEL_KEYS.has(k)) {
84
- cleaned[k] = v;
78
+ const OPENCLAW_MODEL_KEYS = new Set(['id', 'name', 'contextWindow', 'maxTokens']);
79
+
80
+ // Parall-managed providers: overlay when present in the platform payload.
81
+ // parall-anthropic is deleted when absent (rollback safety — stale provider
82
+ // would route to anthropic-messages transport that older servers can't auth).
83
+ // parall is never deleted — it's the core provider; a malformed/empty
84
+ // response should not wipe out the agent's only LLM access.
85
+ const PARALL_MANAGED_PROVIDERS = ['parall', 'parall-anthropic'] as const;
86
+ for (const providerName of PARALL_MANAGED_PROVIDERS) {
87
+ const rawProvider = platformProviders[providerName];
88
+ const platformProvider =
89
+ rawProvider && typeof rawProvider === 'object' && !Array.isArray(rawProvider)
90
+ ? (rawProvider as Record<string, unknown>)
91
+ : null;
92
+ if (!platformProvider) {
93
+ if (providerName !== 'parall') delete providers[providerName];
94
+ continue;
95
+ }
96
+
97
+ if (Array.isArray(platformProvider.models)) {
98
+ platformProvider.models = (platformProvider.models as Record<string, unknown>[]).map((m) => {
99
+ const cleaned: Record<string, unknown> = {};
100
+ for (const [k, v] of Object.entries(m)) {
101
+ if (OPENCLAW_MODEL_KEYS.has(k)) {
102
+ cleaned[k] = v;
103
+ }
85
104
  }
86
- }
87
- return cleaned;
88
- });
105
+ return cleaned;
106
+ });
107
+ }
108
+
109
+ providers[providerName] = {
110
+ ...((providers[providerName] ?? {}) as Record<string, unknown>),
111
+ ...platformProvider,
112
+ apiKey: credentials.api_key,
113
+ };
89
114
  }
90
115
 
91
- providers.parall = {
92
- ...existingParall,
93
- ...platformParall,
94
- apiKey: credentials.api_key,
95
- };
96
116
  models.providers = providers;
97
117
  existing.models = models;
98
118
 
99
119
  // Strip keys OpenClaw doesn't recognize from agents.defaults — platform-only
100
120
  // keys like thinking_effort are consumed by agent-core's PlatformConfigManager.
101
121
  // See: OpenClaw agents.defaults config schema.
102
- const OPENCLAW_AGENTS_DEFAULTS_KEYS = new Set(["model", "compaction", "memorySearch"]);
122
+ const OPENCLAW_AGENTS_DEFAULTS_KEYS = new Set(['model', 'compaction', 'memorySearch']);
123
+ // `model` is platform-sourced (delivered only on the catalog-gated Parall
124
+ // route) — it must NOT be carried over from the existing file, otherwise a
125
+ // stale `parall/...` model survives a switch to runtime_auth (the server then
126
+ // omits agents.defaults entirely) and the runtime keeps using the Parall
127
+ // provider. compaction / memorySearch are operator-tunable, so preserve those
128
+ // across applies; model is re-sourced from the platform overlay below or
129
+ // dropped when the platform sends none.
130
+ const OPENCLAW_PRESERVE_KEYS = new Set(['compaction', 'memorySearch']);
103
131
 
104
132
  // Unconditionally sanitize existing defaults to self-heal already-poisoned
105
133
  // openclaw.json files (e.g. mode=self agents that were poisoned while in
@@ -108,7 +136,7 @@ function applyToOpenClawConfig(
108
136
  const existingDefaults = (agents.defaults ?? {}) as Record<string, unknown>;
109
137
  const cleanedExisting: Record<string, unknown> = {};
110
138
  for (const [k, v] of Object.entries(existingDefaults)) {
111
- if (OPENCLAW_AGENTS_DEFAULTS_KEYS.has(k)) {
139
+ if (OPENCLAW_PRESERVE_KEYS.has(k)) {
112
140
  cleanedExisting[k] = v;
113
141
  }
114
142
  }
@@ -119,7 +147,7 @@ function applyToOpenClawConfig(
119
147
  if (
120
148
  rawDefaults !== undefined &&
121
149
  rawDefaults !== null &&
122
- typeof rawDefaults === "object" &&
150
+ typeof rawDefaults === 'object' &&
123
151
  !Array.isArray(rawDefaults)
124
152
  ) {
125
153
  const raw = rawDefaults as Record<string, unknown>;
@@ -130,6 +158,19 @@ function applyToOpenClawConfig(
130
158
  }
131
159
  }
132
160
 
161
+ // Bidirectional model rewrite for parall-anthropic. Gate on the fresh
162
+ // platform payload so rollback self-heals without manual intervention.
163
+ if (typeof cleanedExisting.model === 'string') {
164
+ const model = cleanedExisting.model as string;
165
+ if (platformProviders['parall-anthropic']) {
166
+ const fwd = model.match(/^parall\/(anthropic\/.+)$/);
167
+ if (fwd) cleanedExisting.model = `parall-anthropic/${fwd[1]}`;
168
+ } else {
169
+ const rev = model.match(/^parall-anthropic\/(anthropic\/.+)$/);
170
+ if (rev) cleanedExisting.model = `parall/${rev[1]}`;
171
+ }
172
+ }
173
+
133
174
  agents.defaults = cleanedExisting;
134
175
  existing.agents = agents;
135
176
 
@@ -138,7 +179,7 @@ function applyToOpenClawConfig(
138
179
  // Atomic write
139
180
  const tmpPath = `${configPath}.tmp`;
140
181
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
141
- fs.writeFileSync(tmpPath, JSON.stringify(existing, null, 2), "utf-8");
182
+ fs.writeFileSync(tmpPath, JSON.stringify(existing, null, 2), 'utf-8');
142
183
  fs.renameSync(tmpPath, configPath);
143
184
  }
144
185
 
@@ -159,7 +200,9 @@ export async function fetchAndApplyPlatformConfig(opts: ConfigManagerOpts): Prom
159
200
  } catch (err) {
160
201
  // Fetch failed — fall through to use cache
161
202
  if (cached) {
162
- log?.warn(`platform config fetch failed, using cached version ${cached.version}: ${String(err)}`);
203
+ log?.warn(
204
+ `platform config fetch failed, using cached version ${cached.version}: ${String(err)}`,
205
+ );
163
206
  applyToOpenClawConfig(configPath, cached.config, credentials);
164
207
  return;
165
208
  }
@@ -170,7 +213,7 @@ export async function fetchAndApplyPlatformConfig(opts: ConfigManagerOpts): Prom
170
213
 
171
214
  // 3. 304 — config unchanged
172
215
  if (fresh === null) {
173
- log?.info("platform config unchanged (304)");
216
+ log?.info('platform config unchanged (304)');
174
217
  if (cached) {
175
218
  applyToOpenClawConfig(configPath, cached.config, credentials);
176
219
  }
@@ -180,7 +223,9 @@ export async function fetchAndApplyPlatformConfig(opts: ConfigManagerOpts): Prom
180
223
  // 4. Validate schema compatibility before accepting
181
224
  const SUPPORTED_SCHEMA_VERSION = 1;
182
225
  if (fresh.schema_version !== undefined && fresh.schema_version > SUPPORTED_SCHEMA_VERSION) {
183
- log?.error(`platform config schema_version ${fresh.schema_version} is newer than supported (${SUPPORTED_SCHEMA_VERSION}), keeping current config`);
226
+ log?.error(
227
+ `platform config schema_version ${fresh.schema_version} is newer than supported (${SUPPORTED_SCHEMA_VERSION}), keeping current config`,
228
+ );
184
229
  if (cached) {
185
230
  applyToOpenClawConfig(configPath, cached.config, credentials);
186
231
  }
package/src/fork.ts CHANGED
@@ -7,10 +7,10 @@
7
7
  // branch point, the fork falls back to the current on-disk leaf — which
8
8
  // may include partial in-flight state.
9
9
 
10
- import * as fs from "node:fs";
11
- import * as path from "node:path";
12
- import * as crypto from "node:crypto";
13
- import { CURRENT_SESSION_VERSION, SessionManager } from "./oc-session.js";
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import * as crypto from 'node:crypto';
13
+ import { CURRENT_SESSION_VERSION, SessionManager } from './oc-session.js';
14
14
 
15
15
  export type ForkSessionResult = {
16
16
  sessionKey: string;
@@ -25,26 +25,33 @@ type SessionStoreEntry = {
25
25
  };
26
26
 
27
27
  function readStoreEntry(sessionsDir: string, sessionKey: string): SessionStoreEntry | null {
28
- const storeFile = path.join(sessionsDir, "sessions.json");
28
+ const storeFile = path.join(sessionsDir, 'sessions.json');
29
29
  try {
30
- const store = JSON.parse(fs.readFileSync(storeFile, "utf-8")) as Record<string, SessionStoreEntry>;
30
+ const store = JSON.parse(fs.readFileSync(storeFile, 'utf-8')) as Record<
31
+ string,
32
+ SessionStoreEntry
33
+ >;
31
34
  return store[sessionKey] ?? store[sessionKey.toLowerCase()] ?? null;
32
35
  } catch {
33
36
  return null;
34
37
  }
35
38
  }
36
39
 
37
- function writeStoreEntry(sessionsDir: string, sessionKey: string, entry: SessionStoreEntry): boolean {
38
- const storeFile = path.join(sessionsDir, "sessions.json");
40
+ function writeStoreEntry(
41
+ sessionsDir: string,
42
+ sessionKey: string,
43
+ entry: SessionStoreEntry,
44
+ ): boolean {
45
+ const storeFile = path.join(sessionsDir, 'sessions.json');
39
46
  try {
40
47
  let store: Record<string, SessionStoreEntry> = {};
41
48
  try {
42
- store = JSON.parse(fs.readFileSync(storeFile, "utf-8"));
49
+ store = JSON.parse(fs.readFileSync(storeFile, 'utf-8'));
43
50
  } catch {
44
51
  // Empty or missing — start fresh.
45
52
  }
46
53
  store[sessionKey.toLowerCase()] = entry;
47
- fs.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
54
+ fs.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: 'utf-8' });
48
55
  return true;
49
56
  } catch {
50
57
  return false;
@@ -52,12 +59,12 @@ function writeStoreEntry(sessionsDir: string, sessionKey: string, entry: Session
52
59
  }
53
60
 
54
61
  function deleteStoreEntry(sessionsDir: string, sessionKey: string): void {
55
- const storeFile = path.join(sessionsDir, "sessions.json");
62
+ const storeFile = path.join(sessionsDir, 'sessions.json');
56
63
  try {
57
- const store = JSON.parse(fs.readFileSync(storeFile, "utf-8")) as Record<string, unknown>;
64
+ const store = JSON.parse(fs.readFileSync(storeFile, 'utf-8')) as Record<string, unknown>;
58
65
  delete store[sessionKey];
59
66
  delete store[sessionKey.toLowerCase()];
60
- fs.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: "utf-8" });
67
+ fs.writeFileSync(storeFile, JSON.stringify(store, null, 2), { encoding: 'utf-8' });
61
68
  } catch {
62
69
  // Best-effort cleanup.
63
70
  }
@@ -83,7 +90,7 @@ export function resolveTranscriptFile(sessionsDir: string, sessionKey: string):
83
90
 
84
91
  try {
85
92
  const files = fs.readdirSync(sessionsDir);
86
- const match = files.find((file) => file.includes(entry.sessionId!) && file.endsWith(".jsonl"));
93
+ const match = files.find((file) => file.includes(entry.sessionId!) && file.endsWith('.jsonl'));
87
94
  return match ? path.join(sessionsDir, match) : null;
88
95
  } catch {
89
96
  return null;
@@ -139,13 +146,10 @@ export function forkOrchestratorSession(opts: {
139
146
  // Fallback: no leaf node — create a header-only fork file manually.
140
147
  sessionId = crypto.randomUUID();
141
148
  const timestamp = new Date().toISOString();
142
- const fileTimestamp = timestamp.replace(/[:.]/g, "-");
143
- sessionFile = path.join(
144
- manager.getSessionDir(),
145
- `${fileTimestamp}_${sessionId}.jsonl`,
146
- );
149
+ const fileTimestamp = timestamp.replace(/[:.]/g, '-');
150
+ sessionFile = path.join(manager.getSessionDir(), `${fileTimestamp}_${sessionId}.jsonl`);
147
151
  const header = {
148
- type: "session",
152
+ type: 'session',
149
153
  version: CURRENT_SESSION_VERSION,
150
154
  id: sessionId,
151
155
  timestamp,
@@ -153,9 +157,9 @@ export function forkOrchestratorSession(opts: {
153
157
  parentSession: transcriptFile,
154
158
  };
155
159
  fs.writeFileSync(sessionFile, `${JSON.stringify(header)}\n`, {
156
- encoding: "utf-8",
160
+ encoding: 'utf-8',
157
161
  mode: 0o600,
158
- flag: "wx",
162
+ flag: 'wx',
159
163
  });
160
164
  }
161
165