@parall/agent-core 1.37.0 → 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.
@@ -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
+ }
package/dist/types.d.ts CHANGED
@@ -72,6 +72,8 @@ export type ParallEvent = {
72
72
  sentAt?: string;
73
73
  ackSourceType?: 'message' | 'task_activity' | 'comment' | 'schedule_run' | 'external_trigger_run' | 'channel_message';
74
74
  ackSourceId?: string;
75
+ /** WorkItem id, when known (dispatch catch-up / re-drive hints carry it; live message.new does not). */
76
+ dispatchEventId?: string;
75
77
  /** Unread message count in the target chat since agent's last interaction. */
76
78
  unreadCount?: number;
77
79
  /** Channel cursor: the last message ID the agent read. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/agent-core",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "description": "Shared agent runtime orchestration helpers for Parall",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "@opentelemetry/sdk-logs": "^0.57.0",
36
36
  "@opentelemetry/sdk-metrics": "^1.30.0",
37
37
  "@opentelemetry/sdk-trace-node": "^1.30.0",
38
- "@parall/sdk": "1.37.0"
38
+ "@parall/sdk": "1.38.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^22.0.0",
@@ -23,6 +23,12 @@ export type DispatchContext = {
23
23
  contextFilePath?: string;
24
24
  /** @deprecated Use contextFilePath. Kept for runtimes that haven't migrated. */
25
25
  stepIdFilePath?: string;
26
+ /**
27
+ * PRLL_CONTEXT_DIR contract: the per-agent directory holding per-lane
28
+ * dispatch context files (`<lane-key>.json`, see lane-key.ts). Stable for
29
+ * the life of the bridge — safe to pin into spawn env.
30
+ */
31
+ contextDirPath?: string;
26
32
  client: ParallClient;
27
33
  log?: GatewayLogger;
28
34
  };