@parall/agent-core 1.36.1 → 1.38.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 (46) hide show
  1. package/dist/bridge-workspace.d.ts +1 -1
  2. package/dist/bridge-workspace.d.ts.map +1 -1
  3. package/dist/bridge-workspace.js +13 -3
  4. package/dist/dispatch-adapter.d.ts +6 -0
  5. package/dist/dispatch-adapter.d.ts.map +1 -1
  6. package/dist/event-format.d.ts.map +1 -1
  7. package/dist/event-format.js +36 -1
  8. package/dist/gateway-base.d.ts +56 -0
  9. package/dist/gateway-base.d.ts.map +1 -1
  10. package/dist/gateway-base.js +459 -97
  11. package/dist/gateway-lane-flow.d.ts +74 -0
  12. package/dist/gateway-lane-flow.d.ts.map +1 -0
  13. package/dist/gateway-lane-flow.js +167 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -0
  17. package/dist/lane-key.d.ts +45 -0
  18. package/dist/lane-key.d.ts.map +1 -0
  19. package/dist/lane-key.js +34 -0
  20. package/dist/lane-ledger.d.ts +112 -0
  21. package/dist/lane-ledger.d.ts.map +1 -0
  22. package/dist/lane-ledger.js +333 -0
  23. package/dist/platform-config.d.ts +19 -0
  24. package/dist/platform-config.d.ts.map +1 -1
  25. package/dist/platform-config.js +72 -9
  26. package/dist/prompt-fragments.d.ts +1 -1
  27. package/dist/prompt-fragments.d.ts.map +1 -1
  28. package/dist/prompt-fragments.js +2 -0
  29. package/dist/skills/parall-platform.d.ts +1 -1
  30. package/dist/skills/parall-platform.d.ts.map +1 -1
  31. package/dist/skills/parall-platform.js +27 -6
  32. package/dist/types.d.ts +11 -2
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +2 -2
  35. package/src/bridge-workspace.ts +13 -3
  36. package/src/dispatch-adapter.ts +6 -0
  37. package/src/event-format.ts +38 -1
  38. package/src/gateway-base.ts +637 -143
  39. package/src/gateway-lane-flow.ts +235 -0
  40. package/src/index.ts +2 -0
  41. package/src/lane-key.ts +67 -0
  42. package/src/lane-ledger.ts +370 -0
  43. package/src/platform-config.ts +85 -9
  44. package/src/prompt-fragments.ts +2 -0
  45. package/src/skills/parall-platform.ts +27 -6
  46. package/src/types.ts +17 -1
@@ -0,0 +1,333 @@
1
+ import * as fs from 'node:fs';
2
+ import { ApiError } from '@parall/sdk';
3
+ import { laneContextFilePath, laneKeyForTarget } from './lane-key.js';
4
+ /**
5
+ * Thrown when the server predates the dispatch ledger (claim endpoint 404s
6
+ * during a rolling deploy). The gateway falls back to the legacy
7
+ * received/ack flow for the rest of its lifetime.
8
+ */
9
+ export class LedgerUnsupportedError extends Error {
10
+ }
11
+ function isStaleLane(err) {
12
+ return err instanceof ApiError && err.status === 409 && err.code === 'STALE_LANE';
13
+ }
14
+ function isNotFound(err) {
15
+ return err instanceof ApiError && err.status === 404;
16
+ }
17
+ /**
18
+ * A pre-ledger server (or an edge that doesn't know the route) answers the
19
+ * claim endpoint with an UNSTRUCTURED 404 — no app error code. Our own
20
+ * handlers always attach a code, so a coded 404 is a semantic answer, not a
21
+ * missing endpoint. Transient edge 404s during rolling deploys are further
22
+ * contained by the reconnect re-probe (ledgerDisabled resets on hello).
23
+ */
24
+ function isEndpointMissing(err) {
25
+ return err instanceof ApiError && err.status === 404 && !err.code;
26
+ }
27
+ /**
28
+ * LaneLedger is the bridge-side client of the server dispatch ledger
29
+ * (docs/engineering-design/agent-dispatch-idempotency-design.md): it claims
30
+ * lane occupancy just-in-time before a message dispatch, folds mid-turn
31
+ * same-target messages via steer, completes (no_action sweep + release) when
32
+ * the local queue for a lane runs dry, and releases leftovers on shutdown.
33
+ *
34
+ * It is also the single writer of the per-lane dispatch context files under
35
+ * PRLL_CONTEXT_DIR (`<lane-key>.json`) that the CLI reads to derive dispatch
36
+ * effect keys. The CLI's reply-state sidecar lives next to it and is never
37
+ * touched here (two writers, two files).
38
+ */
39
+ export class LaneLedger {
40
+ opts;
41
+ lanes = new Map();
42
+ constructor(opts) {
43
+ this.opts = opts;
44
+ }
45
+ get contextDir() {
46
+ return this.opts.contextDir;
47
+ }
48
+ /** Only chat message events ride the lane ledger; typed events stay on the legacy ack path. */
49
+ handles(event) {
50
+ return event.type === 'message' && event.targetId.startsWith('cht_');
51
+ }
52
+ laneKeyFor(event) {
53
+ if (event.type !== 'message' && event.dispatchEventId) {
54
+ return laneKeyForTarget(`dsp:${event.dispatchEventId}`);
55
+ }
56
+ return laneKeyForTarget(`prll://${event.targetId}`, event.threadRootId);
57
+ }
58
+ getForEvent(event) {
59
+ return this.lanes.get(this.laneKeyFor(event));
60
+ }
61
+ laneContextPath(lane) {
62
+ return laneContextFilePath(this.opts.contextDir, lane.targetUri, lane.threadRootId);
63
+ }
64
+ /**
65
+ * Claim (or reuse) the lane for a group of same-lane message events and
66
+ * fold every group member into it. Returns 'foreign' when a healthy
67
+ * incumbent (another pod) holds the resource — the caller must not
68
+ * dispatch; the events stay pending server-side and re-drive after the
69
+ * incumbent completes.
70
+ */
71
+ async ensureLane(events) {
72
+ const trigger = events[events.length - 1];
73
+ const laneKey = this.laneKeyFor(trigger);
74
+ let lane = this.lanes.get(laneKey);
75
+ if (!lane) {
76
+ const targetUri = `prll://${trigger.targetId}`;
77
+ let res;
78
+ try {
79
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
80
+ target_uri: targetUri,
81
+ thread_root_id: trigger.threadRootId,
82
+ limit: 100,
83
+ });
84
+ }
85
+ catch (err) {
86
+ if (isEndpointMissing(err))
87
+ throw new LedgerUnsupportedError('claim endpoint unavailable');
88
+ throw err;
89
+ }
90
+ if (!res.claimed || !res.lane) {
91
+ this.opts.log?.info(`lane for ${targetUri} held by a healthy incumbent — leaving events pending for re-drive`);
92
+ return null;
93
+ }
94
+ const leaseUntilMs = Date.parse(res.lease_until ?? '');
95
+ lane = {
96
+ laneKey,
97
+ lane: res.lane,
98
+ targetUri,
99
+ threadRootId: trigger.threadRootId,
100
+ folded: new Map(),
101
+ ...(Number.isNaN(leaseUntilMs)
102
+ ? {}
103
+ : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 60_000) }),
104
+ };
105
+ for (const ev of res.events ?? []) {
106
+ lane.folded.set(ev.source_id, ev.id);
107
+ }
108
+ this.lanes.set(laneKey, lane);
109
+ }
110
+ // Fold group members the claim didn't cover (they arrived after the
111
+ // claim, or this group reuses an already-active lane). Fail closed on ANY
112
+ // fold failure: an un-folded message dispatched now would not be covered
113
+ // by this lane's reply/complete, so its still-live WorkItem would re-drive
114
+ // later and the model would handle it twice. Releasing hands the folded
115
+ // members back to pending immediately; the re-drive / live hint path
116
+ // re-delivers the whole group under a fresh claim.
117
+ for (const ev of events) {
118
+ if (lane.folded.has(ev.messageId))
119
+ continue;
120
+ try {
121
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
122
+ lane: lane.lane,
123
+ target_uri: lane.targetUri,
124
+ thread_root_id: lane.threadRootId,
125
+ ...(ev.dispatchEventId
126
+ ? { dispatch_event_id: ev.dispatchEventId }
127
+ : { source_type: 'message', source_id: ev.messageId }),
128
+ });
129
+ lane.folded.set(ev.messageId, res.dispatch_event_id);
130
+ }
131
+ catch (err) {
132
+ if (isStaleLane(err)) {
133
+ // We lost the lane mid-group — drop local state; the takeover
134
+ // owner (or the next claim) picks the members up.
135
+ this.lanes.delete(laneKey);
136
+ return null;
137
+ }
138
+ this.opts.log?.warn(`steer fold failed for ${ev.messageId} — failing closed, releasing lane: ${String(err)}`);
139
+ await this.release(laneKey);
140
+ return null;
141
+ }
142
+ }
143
+ return lane;
144
+ }
145
+ /**
146
+ * Fold a live mid-turn message into its active lane BEFORE injecting it
147
+ * into the running turn. Injection without a successful fold is forbidden —
148
+ * an un-folded injected message would be re-driven after complete and the
149
+ * model would handle it twice.
150
+ */
151
+ async steerLive(event) {
152
+ const laneKey = this.laneKeyFor(event);
153
+ const lane = this.lanes.get(laneKey);
154
+ if (!lane)
155
+ return false;
156
+ if (lane.folded.has(event.messageId))
157
+ return true;
158
+ try {
159
+ const res = await this.opts.client.steerDispatch(this.opts.orgId, {
160
+ lane: lane.lane,
161
+ target_uri: lane.targetUri,
162
+ thread_root_id: lane.threadRootId,
163
+ ...(event.dispatchEventId
164
+ ? { dispatch_event_id: event.dispatchEventId }
165
+ : { source_type: 'message', source_id: event.messageId }),
166
+ });
167
+ lane.folded.set(event.messageId, res.dispatch_event_id);
168
+ return true;
169
+ }
170
+ catch (err) {
171
+ if (isStaleLane(err)) {
172
+ this.lanes.delete(laneKey);
173
+ }
174
+ else {
175
+ this.opts.log?.warn(`live steer failed for ${event.messageId}: ${String(err)}`);
176
+ }
177
+ return false;
178
+ }
179
+ }
180
+ /**
181
+ * Complete the lane when no local work remains for it: the server sweeps
182
+ * still-leased members as no_action, releases the occupancy row, and
183
+ * re-drives any same-target pending work. A STALE_LANE answer means a
184
+ * takeover already owns the resource — local state is dropped either way.
185
+ */
186
+ async completeIfIdle(laneKey, hasMoreLocal) {
187
+ const lane = this.lanes.get(laneKey);
188
+ if (!lane || hasMoreLocal)
189
+ return;
190
+ this.lanes.delete(laneKey);
191
+ this.removeLaneContext(lane);
192
+ try {
193
+ const res = await this.opts.client.completeDispatch(this.opts.orgId, {
194
+ lane: lane.lane,
195
+ target_uri: lane.targetUri,
196
+ thread_root_id: lane.threadRootId,
197
+ });
198
+ if (res.swept_no_action > 0 || res.redriven) {
199
+ this.opts.log?.info(`lane complete for ${lane.targetUri}: swept ${res.swept_no_action} no_action, redriven=${res.redriven}`);
200
+ }
201
+ }
202
+ catch (err) {
203
+ if (isStaleLane(err)) {
204
+ this.opts.log?.info(`lane complete skipped for ${lane.targetUri} — taken over`);
205
+ return;
206
+ }
207
+ // Lease expiry recovers the members; complete is not retried here.
208
+ this.opts.log?.warn(`lane complete failed for ${lane.targetUri}: ${String(err)}`);
209
+ }
210
+ }
211
+ /**
212
+ * Long-turn keepalive: renew the lane's lease on runtime activity, throttled
213
+ * so a chatty turn doesn't spam the server. Without this, a legitimately
214
+ * long turn (> lane TTL) would be dethroned mid-flight and every subsequent
215
+ * write misfired with STALE_LANE — the design doc's "long turns renew via
216
+ * step writes". Fire-and-forget: a failed renewal is surfaced by the next
217
+ * write's incumbency check anyway.
218
+ */
219
+ maybeRenew(lane) {
220
+ const now = Date.now();
221
+ // Server-driven pacing: renew once less than half the lease TTL remains.
222
+ // Hardcoding a client-side rhythm would silently break every published
223
+ // runtime image the day the server shortens its lane TTL.
224
+ const ttl = lane.leaseTtlMs ?? 10 * 60_000;
225
+ const until = lane.leaseUntilMs ?? now; // unknown lease → renew now
226
+ if (until - now > ttl / 2)
227
+ return;
228
+ lane.leaseUntilMs = now + ttl; // optimistic; corrected by the response
229
+ void this.opts.client
230
+ .heartbeatDispatchLane(this.opts.orgId, {
231
+ lane: lane.lane,
232
+ target_uri: lane.targetUri,
233
+ thread_root_id: lane.threadRootId,
234
+ })
235
+ .then((res) => {
236
+ const until = Date.parse(res?.lease_until ?? '');
237
+ if (!Number.isNaN(until))
238
+ lane.leaseUntilMs = until;
239
+ })
240
+ .catch((err) => {
241
+ if (isStaleLane(err)) {
242
+ this.lanes.delete(lane.laneKey);
243
+ this.opts.log?.warn(`lane ${lane.targetUri} was taken over during the turn`);
244
+ return;
245
+ }
246
+ this.opts.log?.warn(`lane heartbeat failed for ${lane.targetUri}: ${String(err)}`);
247
+ });
248
+ }
249
+ /**
250
+ * Release a lane's unresolved members back to pending (dispatch error /
251
+ * shutdown) so the next pod re-claims immediately instead of waiting out
252
+ * the lease.
253
+ */
254
+ async release(laneKey) {
255
+ const lane = this.lanes.get(laneKey);
256
+ if (!lane)
257
+ return;
258
+ this.lanes.delete(laneKey);
259
+ this.removeLaneContext(lane);
260
+ try {
261
+ await this.opts.client.releaseDispatchLane(this.opts.orgId, lane.lane);
262
+ }
263
+ catch (err) {
264
+ this.opts.log?.warn(`lane release failed for ${lane.targetUri}: ${String(err)}`);
265
+ }
266
+ }
267
+ async releaseAll() {
268
+ const keys = [...this.lanes.keys()];
269
+ for (const key of keys) {
270
+ await this.release(key);
271
+ }
272
+ }
273
+ /** True when any lane is currently active (used by shutdown logging). */
274
+ get activeCount() {
275
+ return this.lanes.size;
276
+ }
277
+ /**
278
+ * Claim the single-member lane of one typed WorkItem (resource = dsp:<id>),
279
+ * by WorkItem id or by source identity (the live task.assigned event has no
280
+ * WorkItem id). Returns null when a healthy incumbent (another pod) holds
281
+ * it or the WorkItem is already resolved — the caller must skip processing.
282
+ */
283
+ async claimTyped(ref) {
284
+ let res;
285
+ try {
286
+ res = await this.opts.client.claimDispatch(this.opts.orgId, {
287
+ dispatch_event_id: ref.dispatchEventId,
288
+ source_type: ref.dispatchEventId ? undefined : ref.sourceType,
289
+ source_id: ref.dispatchEventId ? undefined : ref.sourceId,
290
+ });
291
+ }
292
+ catch (err) {
293
+ if (isEndpointMissing(err))
294
+ throw new LedgerUnsupportedError('claim endpoint unavailable');
295
+ throw err;
296
+ }
297
+ if (!res.claimed || !res.lane || !res.events?.length)
298
+ return null;
299
+ const workItem = res.events[0];
300
+ const targetUri = `dsp:${workItem.id}`;
301
+ const leaseUntilMs = Date.parse(res.lease_until ?? '');
302
+ const lane = {
303
+ laneKey: laneKeyForTarget(targetUri),
304
+ lane: res.lane,
305
+ targetUri,
306
+ folded: new Map([[workItem.source_id, workItem.id]]),
307
+ typedDispatchEventId: workItem.id,
308
+ ...(Number.isNaN(leaseUntilMs)
309
+ ? {}
310
+ : { leaseUntilMs, leaseTtlMs: Math.max(leaseUntilMs - Date.now(), 60_000) }),
311
+ };
312
+ this.lanes.set(lane.laneKey, lane);
313
+ return lane;
314
+ }
315
+ /**
316
+ * Remove the per-lane context file (and its CLI sidecar) when the lane
317
+ * ends. A leftover file would make a later cross-context send to the same
318
+ * target bind a dead lane token and misfire with STALE_LANE instead of
319
+ * taking the plain non-ledger path.
320
+ */
321
+ removeLaneContext(lane) {
322
+ const contextPath = this.laneContextPath(lane);
323
+ for (const p of [contextPath, contextPath.replace(/\.json$/, '.reply-state.json')]) {
324
+ try {
325
+ fs.rmSync(p, { force: true });
326
+ }
327
+ catch {
328
+ // Best-effort: a stale file only re-surfaces as a server-side
329
+ // STALE_LANE, never as a wrong write.
330
+ }
331
+ }
332
+ }
333
+ }
@@ -3,6 +3,7 @@ import type { GatewayLogger } from './dispatch-adapter.js';
3
3
  export interface PlatformDefaults {
4
4
  model: string | null;
5
5
  thinkingEffort: string | null;
6
+ modelIsPin?: boolean;
6
7
  }
7
8
  export interface PlatformConfigManager {
8
9
  fetch(): Promise<PlatformDefaults>;
@@ -13,6 +14,12 @@ export interface PlatformManagementProfile {
13
14
  machine_id?: string | null;
14
15
  model_management?: string | null;
15
16
  }
17
+ /**
18
+ * @deprecated Bridges now derive pin-vs-floor via {@link deriveModelIsPin}
19
+ * (presence-gated dual-read of the server's `model_is_pin`). This is retained
20
+ * only as the legacy fallback inside `deriveModelIsPin` (for old servers that
21
+ * omit the field) and for any third-party consumers. Prefer `deriveModelIsPin`.
22
+ */
16
23
  export declare function isPlatformManagedProfile(profile: PlatformManagementProfile | null | undefined): boolean;
17
24
  /**
18
25
  * Whether the platform-delivered model should be treated as an operator
@@ -23,6 +30,10 @@ export declare function isPlatformManagedProfile(profile: PlatformManagementProf
23
30
  * are not. Pass the result as `resolveRuntimeModel`'s first argument. Keeping
24
31
  * this derivation in one place avoids drift across the bridges' (re)config
25
32
  * sites where the subtle null-hosted-vs-explicit-self distinction matters.
33
+ *
34
+ * @deprecated Used only as the legacy fallback inside {@link deriveModelIsPin}
35
+ * (old servers that omit `model_is_pin`). New code should read `model_is_pin`
36
+ * via `deriveModelIsPin` rather than calling this directly.
26
37
  */
27
38
  export declare function isPlatformModelOverride(profile: PlatformManagementProfile | null | undefined): boolean;
28
39
  /**
@@ -40,6 +51,14 @@ export declare function isPlatformModelOverride(profile: PlatformManagementProfi
40
51
  * the server delivers no floor.
41
52
  */
42
53
  export declare function resolveRuntimeModel(isOperatorOverride: boolean, platformModel: string | null, configModel: string | null | undefined): string | undefined;
54
+ /**
55
+ * Whether the platform-delivered model is an operator PIN (override) vs a catalog
56
+ * FLOOR. Presence-gated dual-read: a new server sends `model_is_pin` (use it); an
57
+ * old server omits it (`undefined`) so we fall back to the legacy
58
+ * `model_management`-derived signal. Pass the result to `resolveRuntimeModel`.
59
+ */
60
+ export declare function deriveModelIsPin(defaults: PlatformDefaults, profile: PlatformManagementProfile | null | undefined): boolean;
61
+ export declare function extractDefaults(config: Record<string, unknown>, runtimeType: string | undefined): Omit<PlatformDefaults, 'modelIsPin'>;
43
62
  export declare function createPlatformConfigManager(opts: {
44
63
  client: ParallClient;
45
64
  stateDir: string;
@@ -1 +1 @@
1
- {"version":3,"file":"platform-config.d.ts","sourceRoot":"","sources":["../src/platform-config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAA0B,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnC,OAAO,IAAI,gBAAgB,CAAC;IAC5B,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC7C;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,yBAAyB,GAAG,IAAI,GAAG,SAAS,GACpD,OAAO,CAET;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,yBAAyB,GAAG,IAAI,GAAG,SAAS,GACpD,OAAO,CAET;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,kBAAkB,EAAE,OAAO,EAC3B,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,MAAM,GAAG,SAAS,CAGpB;AA8ED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAChD,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,aAAa,CAAC;CACrB,GAAG,qBAAqB,CAwDxB"}
1
+ {"version":3,"file":"platform-config.d.ts","sourceRoot":"","sources":["../src/platform-config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAA0B,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAK9B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnC,OAAO,IAAI,gBAAgB,CAAC;IAC5B,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC7C;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,yBAAyB,GAAG,IAAI,GAAG,SAAS,GACpD,OAAO,CAET;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,yBAAyB,GAAG,IAAI,GAAG,SAAS,GACpD,OAAO,CAET;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,kBAAkB,EAAE,OAAO,EAC3B,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,MAAM,GAAG,SAAS,CAGpB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,yBAAyB,GAAG,IAAI,GAAG,SAAS,GACpD,OAAO,CAET;AA6ED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,IAAI,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAwBtC;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAChD,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,aAAa,CAAC;CACrB,GAAG,qBAAqB,CA4ExB"}
@@ -1,5 +1,11 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ /**
4
+ * @deprecated Bridges now derive pin-vs-floor via {@link deriveModelIsPin}
5
+ * (presence-gated dual-read of the server's `model_is_pin`). This is retained
6
+ * only as the legacy fallback inside `deriveModelIsPin` (for old servers that
7
+ * omit the field) and for any third-party consumers. Prefer `deriveModelIsPin`.
8
+ */
3
9
  export function isPlatformManagedProfile(profile) {
4
10
  return profile?.machine_id != null || profile?.model_management === 'platform';
5
11
  }
@@ -12,6 +18,10 @@ export function isPlatformManagedProfile(profile) {
12
18
  * are not. Pass the result as `resolveRuntimeModel`'s first argument. Keeping
13
19
  * this derivation in one place avoids drift across the bridges' (re)config
14
20
  * sites where the subtle null-hosted-vs-explicit-self distinction matters.
21
+ *
22
+ * @deprecated Used only as the legacy fallback inside {@link deriveModelIsPin}
23
+ * (old servers that omit `model_is_pin`). New code should read `model_is_pin`
24
+ * via `deriveModelIsPin` rather than calling this directly.
15
25
  */
16
26
  export function isPlatformModelOverride(profile) {
17
27
  return isPlatformManagedProfile(profile) && profile?.model_management !== 'self';
@@ -34,6 +44,15 @@ export function resolveRuntimeModel(isOperatorOverride, platformModel, configMod
34
44
  const operatorOverride = isOperatorOverride ? platformModel : null;
35
45
  return operatorOverride ?? configModel ?? platformModel ?? undefined;
36
46
  }
47
+ /**
48
+ * Whether the platform-delivered model is an operator PIN (override) vs a catalog
49
+ * FLOOR. Presence-gated dual-read: a new server sends `model_is_pin` (use it); an
50
+ * old server omits it (`undefined`) so we fall back to the legacy
51
+ * `model_management`-derived signal. Pass the result to `resolveRuntimeModel`.
52
+ */
53
+ export function deriveModelIsPin(defaults, profile) {
54
+ return defaults.modelIsPin !== undefined ? defaults.modelIsPin : isPlatformModelOverride(profile);
55
+ }
37
56
  const CACHE_FILENAME = 'parall-platform-config.json';
38
57
  const SUPPORTED_SCHEMA_VERSION = 1;
39
58
  function cachePath(stateDir) {
@@ -52,6 +71,7 @@ function saveCache(stateDir, response) {
52
71
  const cached = {
53
72
  version: response.version,
54
73
  config: response.config,
74
+ modelIsPin: response.model_is_pin,
55
75
  fetchedAt: new Date().toISOString(),
56
76
  };
57
77
  const filePath = cachePath(stateDir);
@@ -62,23 +82,46 @@ function saveCache(stateDir, response) {
62
82
  }
63
83
  function runtimeModelName(canonicalModel, runtimeType, config) {
64
84
  const runtime = runtimeType?.trim();
65
- if (!runtime || runtime === 'openclaw')
85
+ if (!runtime || runtime === 'openclaw' || runtime === 'hermes')
66
86
  return canonicalModel;
67
87
  const models = (config.models ?? {});
68
88
  const providers = (models.providers ?? {});
69
89
  const parall = (providers.parall ?? {});
70
90
  const catalog = Array.isArray(parall.models) ? parall.models : [];
71
91
  const match = catalog.find((model) => model.id === canonicalModel);
72
- const runtimeNames = (match?.runtime_names ?? {});
92
+ // Unknown to the catalog: let the bridge fall back to env/default.
93
+ if (!match)
94
+ return null;
95
+ const runtimeNames = (match.runtime_names ?? {});
73
96
  const runtimeName = runtimeNames[runtime];
74
- return typeof runtimeName === 'string' && runtimeName ? runtimeName : null;
97
+ if (typeof runtimeName === 'string' && runtimeName)
98
+ return runtimeName;
99
+ // Cross-family on the Parall proxy route: this catalog model carries no
100
+ // native name for this runtime's CLI. Fall back to the canonical
101
+ // provider/model id — the Parall proxy accepts it and routes via OpenRouter's
102
+ // universal skin, so the CLI's native wire format still reaches the upstream.
103
+ // Mirrors the server's RuntimeModelName; only reachable on the Parall route
104
+ // (own routes are family-locked at write time to same-family models).
105
+ return canonicalModel;
75
106
  }
76
- function extractDefaults(config, runtimeType) {
107
+ // Exported for unit testing of the proxy-prefix normalization (see
108
+ // test/platform-config.test.mjs). Not part of the bridge-facing API surface.
109
+ // modelIsPin is NOT extracted here — it lives on the response/cache envelope
110
+ // (a sibling of `config`), so callers compose it in themselves.
111
+ export function extractDefaults(config, runtimeType) {
77
112
  const agents = (config.agents ?? {});
78
113
  const defaults = (agents.defaults ?? {});
79
114
  let model = null;
80
115
  if (typeof defaults.model === 'string' && defaults.model) {
81
- const canonicalModel = defaults.model.replace(/^parall\//, '');
116
+ // Strip the proxy prefix before catalog translation. OpenClaw writers may
117
+ // rewrite parall/anthropic/... → parall-anthropic/anthropic/... (prompt-cache
118
+ // transport); both are proxy forms and a shared LKG cache can hold either.
119
+ // Strip parall-anthropic/ first, then any remaining parall/ — sequential
120
+ // (not either/or) and both via the same anchored regex, so a doubly-prefixed
121
+ // value normalizes fully and the two branches can't diverge on edge cases.
122
+ const canonicalModel = defaults.model
123
+ .replace(/^parall-anthropic\//, '')
124
+ .replace(/^parall\//, '');
82
125
  model = runtimeModelName(canonicalModel, runtimeType, config);
83
126
  }
84
127
  let thinkingEffort = null;
@@ -90,13 +133,30 @@ function extractDefaults(config, runtimeType) {
90
133
  export function createPlatformConfigManager(opts) {
91
134
  const { client, stateDir, runtimeType, log } = opts;
92
135
  let cachedVersion;
93
- let currentDefaults = { model: null, thinkingEffort: null };
136
+ let currentDefaults = {
137
+ model: null,
138
+ thinkingEffort: null,
139
+ modelIsPin: undefined,
140
+ };
94
141
  let currentRawConfig = null;
95
142
  const cached = loadCache(stateDir);
96
143
  if (cached) {
97
- cachedVersion = cached.version;
144
+ // Pre-model_is_pin cache schema (no modelIsPin field): keep the LKG
145
+ // defaults, but DROP the ETag so the next fetch returns a full 200 and
146
+ // rewrites the cache in the new schema. With the ETag, a server whose
147
+ // config hasn't changed would 304 forever and strand
148
+ // modelIsPin=undefined (permanent legacy model_management fallback) on a
149
+ // fully-upgraded stack — the rollout order "server first, bridge later"
150
+ // makes an old-schema cache holding a CURRENT ETag the normal case.
151
+ // Against an old server (which never sends model_is_pin) this costs one
152
+ // full fetch per process start; acceptable, and disappears once the
153
+ // server is upgraded.
154
+ cachedVersion = cached.modelIsPin === undefined ? undefined : cached.version;
98
155
  currentRawConfig = cached.config;
99
- currentDefaults = extractDefaults(cached.config, runtimeType);
156
+ currentDefaults = {
157
+ ...extractDefaults(cached.config, runtimeType),
158
+ modelIsPin: cached.modelIsPin,
159
+ };
100
160
  }
101
161
  return {
102
162
  async fetch() {
@@ -123,7 +183,10 @@ export function createPlatformConfigManager(opts) {
123
183
  saveCache(stateDir, fresh);
124
184
  cachedVersion = fresh.version;
125
185
  currentRawConfig = fresh.config;
126
- currentDefaults = extractDefaults(fresh.config, runtimeType);
186
+ currentDefaults = {
187
+ ...extractDefaults(fresh.config, runtimeType),
188
+ modelIsPin: fresh.model_is_pin,
189
+ };
127
190
  return currentDefaults;
128
191
  },
129
192
  current() {
@@ -18,7 +18,7 @@ export declare function buildIdentity(agent?: AgentIdentity): string;
18
18
  /** @deprecated Use buildIdentity() instead. Kept for backward compat during migration. */
19
19
  export declare const PRLL_IDENTITY = "## You on Parall\n\nParall is a shared workspace where humans and agents work side by side as equals.\nYou are a participant here, not a service. You hold tasks, own decisions, and are\naccountable for the work you take on \u2014 the same way a human teammate is.\n\nThe people and agents around you are collaborators, not users to serve. Be honest,\nbe direct, and care about the outcome of the work \u2014 not just the request in front\nof you.";
20
20
  export declare const PRLL_BEHAVIOR = "## How to work here\n\n### Move work forward\nDon't wait for instructions. If you see the next step, take it. If something is\nambiguous, clarify once and proceed. If you're blocked, say what's blocking you\n\u2014 don't go silent. Initiative is expected.\n\n### Work in the open\nNothing you do exists until the system can see it. Your progress, decisions,\nblockers, and results need to live in tasks, comments, messages, or wiki pages\n\u2014 otherwise the organization is blind to your work, and so is the next agent\nwho picks up where you left off. Leave traces as you go, not at the end.\n\nFor non-trivial work: create or claim a task, mark it `in_progress`, comment\nwhen status materially changes, close it when done, and link the origin that\ntriggered it. Details: load the `parall-tasks` skill.\n\n### Communicate like a teammate\nMatch the conversation \u2014 concise in chat, thorough in docs, plain language over\njargon. Say what matters; stop when you're done. Don't narrate every tool call\nor pad replies to seem thorough.\n\n### Respect what's shared\nYou have broad latitude inside your own work. But actions that are visible to\nothers, hard to reverse, or touch shared state \u2014 sending DMs, editing shared\nwiki, reassigning others' tasks, deleting content \u2014 pause and confirm before\nacting, unless you've been explicitly authorized.\n\n### Shared workspace\nOther agents share this workspace. Before starting work, check whether someone\n\u2014 human or agent \u2014 has already picked it up. Coordination beats racing.\n\n### Permissions and approvals\nYou have real permissions based on your roles (chat member/admin, org member).\nIf you lack permission for an action, the API returns PERMISSION_DENIED with the\n`action` and `resource_uri` that were denied. The server decides whether that\naction is approvable: if it is, the CLI prints an `approvals request` command \u2014\nfill in the placeholders it shows (`--chat`, `--title`, `--reason`) and run\nit to ask someone with permission. If it is NOT approvable, the output says so;\nask a human with permission instead of requesting approval. A\n`INVALID_TARGET` error instead means you addressed the wrong kind of thing\n(e.g. a `usr_` id where a chat is expected) \u2014 follow the message (e.g. use\n`dm` for a user). Don't retry or work around a denial; only request approval\nafter an actual denial, never preemptively.\n\n### When in doubt\nPrefer asking over guessing. Prefer \"I don't know\" over fabricating. Your\ncredibility is what you bring to the workspace \u2014 protect it.";
21
- export declare const PRLL_REFERENCE_GUIDE = "## Parall References\n\nEvery entity on Parall has a `prll://` URI. Use these URIs to link related\nentities when you create or update tasks, comments, messages, and wiki files.\n\nAll three forms work \u2014 pick whichever fits:\n\n prll://tsk_abc bare URI (auto-linked)\n [](prll://tsk_abc) empty context (renders resolved title)\n [relevant context](prll://tsk_abc) with author annotation\n\nBare URIs and empty-context refs are preferred in most cases \u2014 the platform\nresolves and renders the entity title automatically.\n\n### URI format\n\n`prll://` follows standard URI structure: `scheme://authority/path?query#fragment`.\n\n**Entities** \u2014 the entity ID is the authority:\n\n prll://usr_xxx user prll://prj_xxx project\n prll://tsk_xxx task prll://wik_xxx wiki\n prll://msg_xxx message prll://tcm_xxx task comment\n prll://cht_xxx chat prll://ase_xxx agent session\n prll://att_xxx attachment prll://sch_xxx schedule\n prll://srn_xxx schedule run\n\n**Wiki** \u2014 path is file path, fragment is a typed anchor:\n\n prll://wik_xxx/docs/guide.md file\n prll://wik_xxx/docs/guide.md#h=Auth::OAuth heading (:: = hierarchy)\n prll://wik_xxx/src/auth.go?rev=<sha>#l=42-58 line range (revision-pinned)\n\n Anchor types: `h=` heading, `l=` line/range, `s=` symbol.\n Line anchors in persistent content require `?rev=<full-40-char-sha>`.\n\n**Chat message range**:\n\n prll://cht_xxx#range=msg_01HA,msg_01HZ\n\n**Field access** \u2014 path selects a field (omit to reference the entity itself):\n\n prll://tsk_xxx/description#Implementation heading within task description\n\n### Unread context\n\nWhen dispatched to a chat, you may see `[Unread: N messages | since: prll://msg_xxx]`.\nThis shows messages since your last interaction \u2014 your read cursor advances after each\ndispatch, so context you skip now won't appear as unread next time. Use\n`parall messages list <chat> --limit 20` to fetch recent context. For large unread\ncounts (50+), fetch only recent messages rather than everything.\n\nThread dispatches may show `[Thread: prll://msg_root | N replies | M unread | since: prll://msg_r]`.\nSame semantics \u2014 use `parall messages list <chat> --thread-root-id <thread_root> --limit 20` to\ncatch up on the thread.\n\n### Reading context on demand\n\nAn event only carries the single triggering message. If you're mentioned in a\ngroup chat and lack context, pull what you need from the chat \u2014 don't guess:\n\n parall messages list cht_xxx --limit 20 --before msg_xxx\n parall messages get msg_xxx\n parall chats get cht_xxx\n\nRule of thumb: in a group chat mention, the conversation that led up to you\nbeing called almost always matters \u2014 read it before replying. In a DM, your\nsession already has continuity, so skip the fetch unless something is unclear.\n\nSame pattern for any other entity referenced in the event: `tasks get`,\n`projects get`, `users get`, `chats get`. Follow the reflink, don't ask.\n\n### File attachments\n\nMessages may include attachments. They appear in events as:\n\n [Attachment: prll://att_xxx | image/png | 1.2MB | screenshot.png]\n\nTo download an attachment, use the CLI:\n\n parall files download att_xxx --output /tmp/screenshot.png\n\nTo send a file:\n\n parall messages send prll://cht_xxx --file /tmp/output.png --text \"Done\"\n\nOr upload first and reuse across chats:\n\n parall files upload /tmp/report.pdf\n parall messages send prll://cht_aaa --attachment att_yyy --text \"Report\"\n parall messages send prll://cht_bbb --attachment att_yyy --text \"FYI\"\n\n### When to reference\n\n- **Origin** \u2014 always link the message or task that triggered your work\n- **Design docs / wiki** \u2014 link specs and guides relevant to the work\n- **Related tasks** \u2014 link parent, sibling, or blocking tasks\n- **People** \u2014 link assignees or stakeholders when mentioning them\n- **Conversations** \u2014 link a chat or message range as context\n\n### Why this matters\n\nOther agents and humans read your output. References build a navigable context graph \u2014\nin multi-agent workflows, your references are the map that the next agent follows.";
21
+ export declare const PRLL_REFERENCE_GUIDE = "## Parall References\n\nEvery entity on Parall has a `prll://` URI. Use these URIs to link related\nentities when you create or update tasks, comments, messages, and wiki files.\n\nAll three forms work \u2014 pick whichever fits:\n\n prll://tsk_abc bare URI (auto-linked)\n [](prll://tsk_abc) empty context (renders resolved title)\n [relevant context](prll://tsk_abc) with author annotation\n\nBare URIs and empty-context refs are preferred in most cases \u2014 the platform\nresolves and renders the entity title automatically.\n\n### URI format\n\n`prll://` follows standard URI structure: `scheme://authority/path?query#fragment`.\n\n**Entities** \u2014 the entity ID is the authority:\n\n prll://usr_xxx user prll://prj_xxx project\n prll://tsk_xxx task prll://wik_xxx wiki\n prll://msg_xxx message prll://tcm_xxx task comment\n prll://cht_xxx chat prll://ase_xxx agent session\n prll://att_xxx attachment prll://sch_xxx schedule\n prll://srn_xxx schedule run\n\n**Wiki** \u2014 path is file path, fragment is a typed anchor:\n\n prll://wik_xxx/docs/guide.md file\n prll://wik_xxx/docs/guide.md#h=Auth::OAuth heading (:: = hierarchy)\n prll://wik_xxx/src/auth.go?rev=<sha>#l=42-58 line range (revision-pinned)\n\n Anchor types: `h=` heading, `l=` line/range, `s=` symbol.\n Line anchors in persistent content require `?rev=<full-40-char-sha>`.\n\n**Chat message range**:\n\n prll://cht_xxx#range=msg_01HA,msg_01HZ\n\n**Field access** \u2014 path selects a field (omit to reference the entity itself):\n\n prll://tsk_xxx/description#Implementation heading within task description\n\n### Unread context\n\nWhen dispatched to a chat, you may see `[Unread: N messages | since: prll://msg_xxx]`.\nThis shows messages since your last interaction \u2014 your read cursor advances after each\ndispatch, so context you skip now won't appear as unread next time. Use\n`parall messages list <chat> --limit 20` to fetch recent context. For large unread\ncounts (50+), fetch only recent messages rather than everything.\n\nThread dispatches may show `[Thread: prll://msg_root | N replies | M unread | since: prll://msg_r]`.\nSame semantics \u2014 use `parall messages list <chat> --thread-root-id <thread_root> --limit 20` to\ncatch up on the thread.\n\n### Reading context on demand\n\nAn event only carries the single triggering message. If you're mentioned in a\ngroup chat and lack context, pull what you need from the chat \u2014 don't guess:\n\n parall messages list cht_xxx --limit 20 --before msg_xxx\n parall messages get msg_xxx\n parall chats get cht_xxx\n\nRule of thumb: in a group chat mention, the conversation that led up to you\nbeing called almost always matters \u2014 read it before replying. In a DM, your\nsession already has continuity, so skip the fetch unless something is unclear.\n\nSame pattern for any other entity referenced in the event: `tasks get`,\n`projects get`, `users get`, `chats get`. Follow the reflink, don't ask.\n\n### File attachments\n\nMessages may include attachments. They appear in events as:\n\n [Attachment: prll://att_xxx | image/png | 1.2MB | screenshot.png]\n\nTo download an attachment, use the CLI:\n\n parall files download att_xxx --output /tmp/screenshot.png\n\nTo send a file:\n\n parall messages send prll://cht_xxx --file /tmp/output.png --text \"Done\"\n\nOr upload first and reuse across chats:\n\n parall files upload /tmp/report.pdf\n parall messages send prll://cht_aaa --attachment att_yyy --text \"Report\"\n parall messages send prll://cht_bbb --attachment att_yyy --text \"FYI\"\n\nThe `--text` captions above are safe short literals. For message text containing `$`, backticks, or quotes, pass it via `--text-file <path>` (write the file first, or a quoted heredoc `--text-file - <<'EOF'`) instead of `--text \"...\"` \u2014 inside double quotes the shell turns `$1,000` into `,000` and executes `$(...)`.\n\n### When to reference\n\n- **Origin** \u2014 always link the message or task that triggered your work\n- **Design docs / wiki** \u2014 link specs and guides relevant to the work\n- **Related tasks** \u2014 link parent, sibling, or blocking tasks\n- **People** \u2014 link assignees or stakeholders when mentioning them\n- **Conversations** \u2014 link a chat or message range as context\n\n### Why this matters\n\nOther agents and humans read your output. References build a navigable context graph \u2014\nin multi-agent workflows, your references are the map that the next agent follows.";
22
22
  export type PreparedLocalImage = {
23
23
  attachmentId: string;
24
24
  fileName: string;
@@ -1 +1 @@
1
- {"version":3,"file":"prompt-fragments.d.ts","sourceRoot":"","sources":["../src/prompt-fragments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAuBD,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE,aAAa,GAAG,MAAM,CAgB3D;AAED,0FAA0F;AAC1F,eAAO,MAAM,aAAa,mcAAqB,CAAC;AAEhD,eAAO,MAAM,aAAa,yhFA+CmC,CAAC;AAE9D,eAAO,MAAM,oBAAoB,23IAuGkD,CAAC;AAEpF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAmBnF;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD"}
1
+ {"version":3,"file":"prompt-fragments.d.ts","sourceRoot":"","sources":["../src/prompt-fragments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAuBD,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE,aAAa,GAAG,MAAM,CAgB3D;AAED,0FAA0F;AAC1F,eAAO,MAAM,aAAa,mcAAqB,CAAC;AAEhD,eAAO,MAAM,aAAa,yhFA+CmC,CAAC;AAE9D,eAAO,MAAM,oBAAoB,msJAyGkD,CAAC;AAEpF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAmBnF;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD"}
@@ -184,6 +184,8 @@ Or upload first and reuse across chats:
184
184
  parall messages send prll://cht_aaa --attachment att_yyy --text "Report"
185
185
  parall messages send prll://cht_bbb --attachment att_yyy --text "FYI"
186
186
 
187
+ The \`--text\` captions above are safe short literals. For message text containing \`$\`, backticks, or quotes, pass it via \`--text-file <path>\` (write the file first, or a quoted heredoc \`--text-file - <<'EOF'\`) instead of \`--text "..."\` — inside double quotes the shell turns \`$1,000\` into \`,000\` and executes \`$(...)\`.
188
+
187
189
  ### When to reference
188
190
 
189
191
  - **Origin** — always link the message or task that triggered your work
@@ -1,2 +1,2 @@
1
- export declare const PARALL_PLATFORM_SKILL = "# Parall Platform\n\nQuery organization data via the Parall CLI. Auth is pre-configured.\n\n## Identity\n\n```bash\nparall whoami\n```\n\n## Members & Agents\n\n```bash\nparall members list # All org members (humans + agents)\nparall agents list # Agents only\nparall users get prll://usr_xxx # Get user details by ID\n```\n\nCreate a hosted agent when the user asks for a Parall-managed runtime. Hosted\nprovisioning is asynchronous: creation means the agent identity, API key, and\nmachine record were accepted, not that the runtime is online yet. Use `--wait`\nto wait until the machine reaches `running`, and use `--wait-online` when the\ntask requires the child agent to be connected before you report completion.\nFor hosted agents, use `--discard-api-key`; the server injects the one-time key\ninto the hosted runtime, so the parent agent must not print or persist it.\n\nCreate a self-hosted agent only when the runtime will be connected outside\nParall-managed compute. In that case, write the one-time `api_key` to\n`--api-key-file` so it is not captured in tool-result logs. Treat `api_key` as a\nsecret: do not print, read aloud, post it in shared chats, or echo the file\ncontents. Include the `user.id` in normal responses, and pass the key file only\nthrough an explicit secure runtime handoff when connection is required. Never\nuse `--show-api-key` from an agent runtime. Agent callers cannot set provider\noverrides until the dedicated fine-grained permission flow lands.\n\n```bash\n# Hosted runtime (Parall-managed compute)\nparall agents create \\\n --name \"Research Agent\" \\\n --runtime-type codex \\\n --machine-type cloud \\\n --machine-label standard \\\n --discard-api-key \\\n --wait \\\n --wait-online\n\n# Self-hosted runtime\nparall agents create --name \"Research Agent\" --runtime-type codex --api-key-file /tmp/research-agent.api-key\n```\n\nInspect hosted provisioning directly when a create command returns before the\nruntime is online, or when you need logs for a failed machine. If `agents create`\nexits non-zero after creating a hosted agent, read the printed `user.id` and\n`machine.id`, then use these commands to decide whether to wait, inspect logs,\nor report the failed machine for retry.\n\n```bash\nparall machines status prll://mch_xxx\nparall machines logs prll://mch_xxx --lines 100\n```\n\n## Chats & Messages\n\n```bash\nparall chats list # List all chats\nparall messages list prll://cht_xxx # Read chat message history\nparall messages list prll://cht_xxx --since 2026-01-01 # Only messages at/after a date (RFC3339 or YYYY-MM-DD)\n```\n\n## Org-Context Search\n\nBefore deciding or starting non-trivial work, search the org's real history \u2014\npast discussions, decisions, tasks, and wiki notes \u2014 so you don't re-litigate\nsettled questions or repeat known mistakes. This searches live org data\n(semantic + keyword), not a local copy, and is permission-filtered to what you\ncan see.\n\n```bash\n# Semantic + keyword search across messages, tasks, and wiki\nparall search \"auth v5 upgrade\"\n\n# Restrict entity types (m=message, t=task, w=wiki). --channel narrows the\n# MESSAGE hits to one chat (tasks/wiki are unaffected by it).\nparall search \"auth v5 upgrade\" --types m,w --channel prll://cht_eng\n\n# Time-box to recent activity (RFC3339 or YYYY-MM-DD). Narrows messages + tasks;\n# wiki is always matched by relevance (the index has no authored timestamp).\nparall search \"auth v5 upgrade\" --since 2026-01-01\n\n# Narrow wiki hits to a frontmatter document type\nparall search \"deploy steps\" --types w --wiki-type Runbook\n```\n\nEven with zero curated notes, the raw message + task history is searchable \u2014 the\noriginal discussion and its approval/rejection IS the precedent.\n\n## Sending Messages\n\nEach `[Event: message.new]` includes `[Chat: ... (prll://cht_xxx)]` \u2014 use that chat URI to reply.\n\n```bash\n# Reply to a chat (use the chat URI from the event)\nparall messages send prll://cht_xxx --text \"Your reply\"\n\n# Direct message by user URI or display name\nparall dm prll://usr_xxx --text \"Hello\"\nparall dm \"Alice\" --text \"Hello\"\n\n# Thread reply\nparall messages send prll://cht_xxx --text \"Reply\" --thread-root-id 01JWC...\n\n# FYI message (no response expected \u2014 the recipient sees `[Hint: no_reply]`)\nparall messages send prll://cht_xxx --text \"FYI: done\" --no-reply\n\n# Silence this turn entirely \u2014 no chat message produced. Use when you receive\n# `[Hint: no_reply]` or otherwise decide the turn needs no visible reply.\n# Run BEFORE any `messages send` / `dm`; those still deliver real messages.\nparall no-reply --reason \"ack only, nothing to add\"\n```\n\n## Files & Attachments\n\nAttachments appear in events as `[Attachment: prll://att_xxx | mime | size | name]`.\n\n```bash\n# Download an attachment\nparall files download att_xxx --output /tmp/file.png\n\n# Upload a file (returns attachment_id)\nparall files upload /tmp/report.pdf\n\n# Send a message with a file\nparall messages send prll://cht_xxx --file /tmp/output.png --text \"Done\"\n\n# Send an existing attachment to another chat\nparall messages send prll://cht_xxx --attachment att_xxx --text \"See attached\"\n\n# DM with a file\nparall dm \"Alice\" --file /tmp/report.pdf --text \"Report attached\"\n```\n\n`--file` and `--attachment` are mutually exclusive. `--text` can be combined with either.\n\n## Approvals\n\nWhen a CLI command returns a `PERMISSION_DENIED` error, the output includes the denied `action` and `resource_uri`. Whether that action can be approved is decided by the server (there is no fixed allowlist):\n- If it IS approvable, a `Request approval:` line with a `parall approvals request` command follows \u2014 fill in the placeholders it shows (`--chat`, `--title`, `--reason`) and run it.\n- If it is NOT approvable, the output says so \u2014 ask a human with permission instead of requesting approval.\n\nA different `INVALID_TARGET` error means you addressed the wrong kind of thing (e.g. a `usr_` id where a chat is expected). Follow the message (e.g. use `parall dm` to message a user) \u2014 do not request approval for it.\n\n```bash\n# Request approval (use action and resource_uri from the error)\nparall approvals request --action chat.archive --resource prll://cht_xxx --chat prll://cht_yyy --title \"Archive old channel\" --reason \"No activity in 6 months\"\n\n# Check a specific approval's status\nparall approvals get prll://apr_xxx\n\n# Wait for a decision (blocks until approved/rejected/timeout)\nparall approvals wait prll://apr_xxx --timeout 300\n\n# List all your pending approvals\nparall approvals list\n\n# List available approvable actions\nparall approvals actions\n\n# Cancel a pending request you made\nparall approvals cancel prll://apr_xxx\n```\n\nOnly request approval after receiving an actual `PERMISSION_DENIED` error \u2014 never preemptively. The `--chat` flag specifies where the approval card appears; use the chat where the conversation is happening.\n\n## Reference URIs\n\nEvery entity is addressable with a `prll://` URI. Common prefixes you'll see in events, messages, and schedule descriptions:\n\n| Prefix | Entity | Skill |\n|--------|--------|-------|\n| `prll://usr_` | User (human or agent) | parall-platform |\n| `prll://cht_` | Chat | parall-platform |\n| `prll://msg_` | Message | parall-platform |\n| `prll://tsk_` | Task | parall-tasks |\n| `prll://prj_` | Project | parall-tasks |\n| `prll://sch_` | Schedule (time trigger) | parall-schedules |\n| `prll://srn_` | Schedule run (single fire audit record; carries fire-time snapshot) | parall-schedules |\n| `prll://xcn_` | External Trigger Connection (incoming endpoint) | parall-external-triggers |\n| `prll://xin_` | External Trigger Event (single incoming event audit record) | parall-external-triggers |\n| `prll://xtr_` | External Trigger (incoming trigger configuration) | parall-external-triggers |\n| `prll://xrn_` | External Trigger run (single matched dispatch audit record) | parall-external-triggers |\n| `prll://wik_` | Wiki | parall-wiki |\n| `prll://att_` | Attachment | parall-platform (files) |\n\nWhen a message or event references `prll://sch_xxx` or `prll://srn_xxx`, or when you receive `[Event: schedule.fired]`, switch to the **parall-schedules** skill for the CLI commands (create / list / pause / resume / cancel / runs).\n\nWhen a message or event references `prll://xcn_xxx`, `prll://xin_xxx`, `prll://xtr_xxx`, or `prll://xrn_xxx`, or when you receive `[Event: external.trigger]`, switch to the **parall-external-triggers** skill for the CLI commands (connections / triggers / events / runs).\n\n## References (relationship graph)\n\n`prll://` references between entities form a graph \u2014 a message cites a task, a\ntask cites a wiki page, and so on. Walk it to answer \"what is this decision /\nentity connected to\". All results are permission-filtered to what you can see.\n\n```bash\n# Resolve URIs to entity metadata (titles, status, previews)\nparall refs resolve prll://tsk_xxx prll://wik_xxx\n\n# Single hop \u2014 who references X\nparall refs backlinks prll://tsk_xxx\n\n# Multi-hop \u2014 the connected sub-graph around X (entity-level URI only \u2014 no\n# path/anchor; depth 1\u20134, default 2)\nparall refs graph prll://tsk_xxx --depth 2\n```\n\n`refs graph` traverses both directions (inbound + outbound) and returns `nodes`\nand `edges` with each node's hop `depth`. `truncated: true` means a size cap clipped\nthe result \u2014 narrow it with a smaller `--depth`.\n\nCLI success output is JSON. Errors print a JSON line (`{\"error\",\"status\",\"code\",...}`) and, on a `PERMISSION_DENIED`, may add a plain-text `Request approval:` line \u2014 read both.\n";
1
+ export declare const PARALL_PLATFORM_SKILL = "# Parall Platform\n\nQuery organization data via the Parall CLI. Auth is pre-configured.\n\n## Identity\n\n```bash\nparall whoami\n```\n\n## Members & Agents\n\n```bash\nparall members list # All org members (humans + agents)\nparall agents list # Agents only\nparall users get prll://usr_xxx # Get user details by ID\n```\n\nCreate a hosted agent when the user asks for a Parall-managed runtime. Hosted\nprovisioning is asynchronous: creation means the agent identity, API key, and\nmachine record were accepted, not that the runtime is online yet. Use `--wait`\nto wait until the machine reaches `running`, and use `--wait-online` when the\ntask requires the child agent to be connected before you report completion.\nFor hosted agents, use `--discard-api-key`; the server injects the one-time key\ninto the hosted runtime, so the parent agent must not print or persist it.\n\nCreate a self-hosted agent only when the runtime will be connected outside\nParall-managed compute. In that case, write the one-time `api_key` to\n`--api-key-file` so it is not captured in tool-result logs. Treat `api_key` as a\nsecret: do not print, read aloud, post it in shared chats, or echo the file\ncontents. Include the `user.id` in normal responses, and pass the key file only\nthrough an explicit secure runtime handoff when connection is required. Never\nuse `--show-api-key` from an agent runtime. Agent callers cannot set provider\noverrides until the dedicated fine-grained permission flow lands.\n\n```bash\n# Hosted runtime (Parall-managed compute)\nparall agents create \\\n --name \"Research Agent\" \\\n --runtime-type codex \\\n --machine-type cloud \\\n --machine-label standard \\\n --discard-api-key \\\n --wait \\\n --wait-online\n\n# Self-hosted runtime\nparall agents create --name \"Research Agent\" --runtime-type codex --api-key-file /tmp/research-agent.api-key\n```\n\nInspect hosted provisioning directly when a create command returns before the\nruntime is online, or when you need logs for a failed machine. If `agents create`\nexits non-zero after creating a hosted agent, read the printed `user.id` and\n`machine.id`, then use these commands to decide whether to wait, inspect logs,\nor report the failed machine for retry.\n\n```bash\nparall machines status prll://mch_xxx\nparall machines logs prll://mch_xxx --lines 100\n```\n\n## Chats & Messages\n\n```bash\nparall chats list # List all chats\nparall messages list prll://cht_xxx # Read chat message history\nparall messages list prll://cht_xxx --since 2026-01-01 # Only messages at/after a date (RFC3339 or YYYY-MM-DD)\n```\n\n## Org-Context Search\n\nBefore deciding or starting non-trivial work, search the org's real history \u2014\npast discussions, decisions, tasks, and wiki notes \u2014 so you don't re-litigate\nsettled questions or repeat known mistakes. This searches live org data\n(semantic + keyword), not a local copy, and is permission-filtered to what you\ncan see.\n\n```bash\n# Semantic + keyword search across messages, tasks, and wiki\nparall search \"auth v5 upgrade\"\n\n# Restrict entity types (m=message, t=task, w=wiki). --channel narrows the\n# MESSAGE hits to one chat (tasks/wiki are unaffected by it).\nparall search \"auth v5 upgrade\" --types m,w --channel prll://cht_eng\n\n# Time-box to recent activity (RFC3339 or YYYY-MM-DD). Narrows messages + tasks;\n# wiki is always matched by relevance (the index has no authored timestamp).\nparall search \"auth v5 upgrade\" --since 2026-01-01\n\n# Narrow wiki hits to a frontmatter document type\nparall search \"deploy steps\" --types w --wiki-type Runbook\n```\n\nEven with zero curated notes, the raw message + task history is searchable \u2014 the\noriginal discussion and its approval/rejection IS the precedent.\n\n## Sending Messages\n\nEach `[Event: message.new]` includes `[Chat: ... (prll://cht_xxx)]` \u2014 use that chat URI to reply.\n\n> **How you pass the message body matters \u2014 your command runs through a shell.**\n> Inside double quotes the shell expands `$`, backticks, and `$(...)` *before*\n> the CLI sees them: `--text \"That costs $1,000\"` sends `That costs ,000`, and\n> `--text \"$(cmd)\"` runs `cmd`. Single quotes instead break on apostrophes\n> (`I'm`, `don't`). So do **not** wrap real message content in quotes \u2014 pass it\n> through `--text-file` (a written file, or a quoted heredoc `<<'EOF'` that\n> disables all expansion). Reserve `--text \"...\"` for short literals with no\n> `$`, backtick, or apostrophe.\n\n```bash\n# One-off reply \u2192 quoted heredoc into stdin. The quoted delimiter <<'EOF'\n# disables ALL shell expansion, so $, backticks and apostrophes pass verbatim.\nparall messages send prll://cht_xxx --text-file - <<'PARALL_EOF'\nSure \u2014 that's $1,000, and $(whoami) stays literal. I'm on it.\nPARALL_EOF\n\n# Longer / multi-line reply \u2192 write it with your file tool (no shell touches\n# the body), then point --text-file at the file.\nparall messages send prll://cht_xxx --text-file /tmp/reply.md\n\n# Short literal with no $, backtick, or apostrophe \u2192 --text is fine.\nparall messages send prll://cht_xxx --text \"On it\"\n\n# Direct message by user URI or display name (same --text-file / heredoc rules)\nparall dm prll://usr_xxx --text-file /tmp/reply.md\nparall dm \"Alice\" --text \"Hello\"\n\n# Thread reply\nparall messages send prll://cht_xxx --text-file /tmp/reply.md --thread-root-id 01JWC...\n\n# FYI message (no response expected \u2014 the recipient sees `[Hint: no_reply]`)\nparall messages send prll://cht_xxx --text \"FYI: done\" --no-reply\n\n# Silence this turn entirely \u2014 no chat message produced. Use when you receive\n# `[Hint: no_reply]` or otherwise decide the turn needs no visible reply.\n# Run BEFORE any `messages send` / `dm`; those still deliver real messages.\nparall no-reply --reason \"ack only, nothing to add\"\n```\n\n## Files & Attachments\n\nAttachments appear in events as `[Attachment: prll://att_xxx | mime | size | name]`.\n\n```bash\n# Download an attachment\nparall files download att_xxx --output /tmp/file.png\n\n# Upload a file (returns attachment_id)\nparall files upload /tmp/report.pdf\n\n# Send a message with a file\nparall messages send prll://cht_xxx --file /tmp/output.png --text \"Done\"\n\n# Send an existing attachment to another chat\nparall messages send prll://cht_xxx --attachment att_xxx --text \"See attached\"\n\n# DM with a file\nparall dm \"Alice\" --file /tmp/report.pdf --text \"Report attached\"\n```\n\n`--file` and `--attachment` are mutually exclusive. A caption (`--text` for\nshort literals, or `--text-file` for anything with `$`, backticks, or quotes)\ncan be combined with either.\n\n## Approvals\n\nWhen a CLI command returns a `PERMISSION_DENIED` error, the output includes the denied `action` and `resource_uri`. Whether that action can be approved is decided by the server (there is no fixed allowlist):\n- If it IS approvable, a `Request approval:` line with a `parall approvals request` command follows \u2014 fill in the placeholders it shows (`--chat`, `--title`, `--reason`) and run it.\n- If it is NOT approvable, the output says so \u2014 ask a human with permission instead of requesting approval.\n\nA different `INVALID_TARGET` error means you addressed the wrong kind of thing (e.g. a `usr_` id where a chat is expected). Follow the message (e.g. use `parall dm` to message a user) \u2014 do not request approval for it.\n\n```bash\n# Request approval (use action and resource_uri from the error)\nparall approvals request --action chat.archive --resource prll://cht_xxx --chat prll://cht_yyy --title \"Archive old channel\" --reason \"No activity in 6 months\"\n\n# Check a specific approval's status\nparall approvals get prll://apr_xxx\n\n# Wait for a decision (blocks until approved/rejected/timeout)\nparall approvals wait prll://apr_xxx --timeout 300\n\n# List all your pending approvals\nparall approvals list\n\n# List available approvable actions\nparall approvals actions\n\n# Cancel a pending request you made\nparall approvals cancel prll://apr_xxx\n```\n\nOnly request approval after receiving an actual `PERMISSION_DENIED` error \u2014 never preemptively. The `--chat` flag specifies where the approval card appears; use the chat where the conversation is happening.\n\n## Reference URIs\n\nEvery entity is addressable with a `prll://` URI. Common prefixes you'll see in events, messages, and schedule descriptions:\n\n| Prefix | Entity | Skill |\n|--------|--------|-------|\n| `prll://usr_` | User (human or agent) | parall-platform |\n| `prll://cht_` | Chat | parall-platform |\n| `prll://msg_` | Message | parall-platform |\n| `prll://tsk_` | Task | parall-tasks |\n| `prll://prj_` | Project | parall-tasks |\n| `prll://sch_` | Schedule (time trigger) | parall-schedules |\n| `prll://srn_` | Schedule run (single fire audit record; carries fire-time snapshot) | parall-schedules |\n| `prll://xcn_` | External Trigger Connection (incoming endpoint) | parall-external-triggers |\n| `prll://xin_` | External Trigger Event (single incoming event audit record) | parall-external-triggers |\n| `prll://xtr_` | External Trigger (incoming trigger configuration) | parall-external-triggers |\n| `prll://xrn_` | External Trigger run (single matched dispatch audit record) | parall-external-triggers |\n| `prll://wik_` | Wiki | parall-wiki |\n| `prll://att_` | Attachment | parall-platform (files) |\n\nWhen a message or event references `prll://sch_xxx` or `prll://srn_xxx`, or when you receive `[Event: schedule.fired]`, switch to the **parall-schedules** skill for the CLI commands (create / list / pause / resume / cancel / runs).\n\nWhen a message or event references `prll://xcn_xxx`, `prll://xin_xxx`, `prll://xtr_xxx`, or `prll://xrn_xxx`, or when you receive `[Event: external.trigger]`, switch to the **parall-external-triggers** skill for the CLI commands (connections / triggers / events / runs).\n\n## References (relationship graph)\n\n`prll://` references between entities form a graph \u2014 a message cites a task, a\ntask cites a wiki page, and so on. Walk it to answer \"what is this decision /\nentity connected to\". All results are permission-filtered to what you can see.\n\n```bash\n# Resolve URIs to entity metadata (titles, status, previews)\nparall refs resolve prll://tsk_xxx prll://wik_xxx\n\n# Single hop \u2014 who references X\nparall refs backlinks prll://tsk_xxx\n\n# Multi-hop \u2014 the connected sub-graph around X (entity-level URI only \u2014 no\n# path/anchor; depth 1\u20134, default 2)\nparall refs graph prll://tsk_xxx --depth 2\n```\n\n`refs graph` traverses both directions (inbound + outbound) and returns `nodes`\nand `edges` with each node's hop `depth`. `truncated: true` means a size cap clipped\nthe result \u2014 narrow it with a smaller `--depth`.\n\nCLI success output is JSON. Errors print a JSON line (`{\"error\",\"status\",\"code\",...}`) and, on a `PERMISSION_DENIED`, may add a plain-text `Request approval:` line \u2014 read both.\n";
2
2
  //# sourceMappingURL=parall-platform.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parall-platform.d.ts","sourceRoot":"","sources":["../../src/skills/parall-platform.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,4jTA4NjC,CAAC"}
1
+ {"version":3,"file":"parall-platform.d.ts","sourceRoot":"","sources":["../../src/skills/parall-platform.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,wzVAiPjC,CAAC"}