@parall/agent-core 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.
- package/dist/bridge-workspace.js +12 -12
- package/dist/dispatch-adapter.d.ts +15 -8
- package/dist/dispatch-adapter.d.ts.map +1 -1
- package/dist/event-format.d.ts +1 -1
- package/dist/event-format.d.ts.map +1 -1
- package/dist/event-format.js +68 -25
- package/dist/gateway-base.d.ts +15 -13
- package/dist/gateway-base.d.ts.map +1 -1
- package/dist/gateway-base.js +662 -312
- package/dist/index.d.ts +15 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -11
- package/dist/internal/attachment-input.d.ts +3 -3
- package/dist/internal/attachment-input.d.ts.map +1 -1
- package/dist/internal/attachment-input.js +61 -58
- package/dist/logger.d.ts +1 -1
- package/dist/platform-config.d.ts +28 -2
- package/dist/platform-config.d.ts.map +1 -1
- package/dist/platform-config.js +42 -11
- package/dist/prompt-fragments.d.ts +1 -1
- package/dist/prompt-fragments.d.ts.map +1 -1
- package/dist/prompt-fragments.js +28 -10
- package/dist/provider-config.d.ts +20 -0
- package/dist/provider-config.d.ts.map +1 -0
- package/dist/provider-config.js +41 -0
- package/dist/routing.d.ts +5 -5
- package/dist/routing.js +6 -6
- package/dist/session-state.d.ts +16 -0
- package/dist/session-state.d.ts.map +1 -1
- package/dist/session-state.js +45 -0
- package/dist/skills/index.d.ts +5 -4
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +28 -21
- package/dist/skills/parall-clips.d.ts +2 -0
- package/dist/skills/parall-clips.d.ts.map +1 -0
- package/dist/skills/parall-clips.js +44 -0
- package/dist/telemetry.d.ts +27 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +205 -0
- package/dist/types.d.ts +18 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -2
- package/src/bridge-workspace.ts +12 -12
- package/src/dispatch-adapter.ts +31 -8
- package/src/event-format.ts +80 -30
- package/src/gateway-base.ts +998 -442
- package/src/index.ts +23 -12
- package/src/internal/attachment-input.ts +127 -100
- package/src/logger.ts +1 -1
- package/src/platform-config.ts +61 -16
- package/src/prompt-fragments.ts +28 -10
- package/src/provider-config.ts +51 -0
- package/src/routing.ts +11 -11
- package/src/session-state.ts +62 -0
- package/src/skills/index.ts +34 -23
- package/src/skills/parall-clips.ts +44 -0
- package/src/telemetry.ts +252 -0
- package/src/types.ts +18 -2
package/dist/gateway-base.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import * as os from
|
|
2
|
-
import * as fs from
|
|
3
|
-
import * as path from
|
|
4
|
-
import { MENTION_ALL_USER_ID } from
|
|
5
|
-
import { buildEventBody, buildEventBodyForForkResult, buildForkResultPrefix, buildForkScopePrefix } from
|
|
6
|
-
import { routeTrigger } from
|
|
7
|
-
import { clearDispatchMessageId, clearDispatchNoReply, clearSessionMessageId, setDispatchMessageId, setDispatchNoReply, setSessionChatId, setSessionMessageId, } from
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { ApiError, MENTION_ALL_USER_ID } from '@parall/sdk';
|
|
5
|
+
import { buildEventBody, buildEventBodyForForkResult, buildForkResultPrefix, buildForkScopePrefix, } from './event-format.js';
|
|
6
|
+
import { routeTrigger } from './routing.js';
|
|
7
|
+
import { clearDispatchMessageId, clearDispatchMetrics, clearDispatchNoReply, clearSessionMessageId, getDispatchMetrics, recordDeliverText, recordMessageSend, recordNoReply, recordToolCall, resetDispatchMetrics, setDispatchMessageId, setDispatchNoReply, setSessionChatId, setSessionMessageId, } from './session-state.js';
|
|
8
|
+
import { isParallSendCommand, isParallNoReplyCommand, extractShellCommand, } from './bridge-workspace.js';
|
|
9
|
+
import { startDispatchSpan, endDispatchSpan, recordDispatchMetric, recordMissingReply, runWithSessionKey, } from './telemetry.js';
|
|
10
|
+
const LIVE_SESSION_STATUSES = new Set(['open', 'active', 'idle']);
|
|
8
11
|
// Parse PRLL_SHUTDOWN_DEADLINE_MS (or any string env value) into a positive
|
|
9
12
|
// integer milliseconds value, or undefined if unset/invalid. Runtimes pass
|
|
10
13
|
// the result into ParallGatewayOptions.shutdownDeadlineMs; leaving it
|
|
@@ -21,17 +24,72 @@ export function parseShutdownDeadlineMs(raw) {
|
|
|
21
24
|
return undefined;
|
|
22
25
|
return Math.floor(n);
|
|
23
26
|
}
|
|
27
|
+
export function parseForkDeadlineMs(raw) {
|
|
28
|
+
if (!raw)
|
|
29
|
+
return undefined;
|
|
30
|
+
const n = Number(raw);
|
|
31
|
+
if (!Number.isFinite(n) || n <= 0 || n > 2_147_483_647)
|
|
32
|
+
return undefined;
|
|
33
|
+
return Math.floor(n);
|
|
34
|
+
}
|
|
35
|
+
export function parseDispatchDeadlineMs(raw) {
|
|
36
|
+
if (!raw)
|
|
37
|
+
return undefined;
|
|
38
|
+
const n = Number(raw);
|
|
39
|
+
if (!Number.isFinite(n) || n < 0 || n > 2_147_483_647)
|
|
40
|
+
return undefined;
|
|
41
|
+
return Math.floor(n);
|
|
42
|
+
}
|
|
24
43
|
function resolveStepTarget(event) {
|
|
25
|
-
if (event.type ===
|
|
26
|
-
return { target_type:
|
|
44
|
+
if (event.type === 'task' || event.targetId.startsWith('tsk_')) {
|
|
45
|
+
return { target_type: 'task', target_id: event.targetId };
|
|
46
|
+
}
|
|
47
|
+
if (event.targetId.startsWith('cht_')) {
|
|
48
|
+
return { target_type: 'chat', target_id: event.targetId };
|
|
27
49
|
}
|
|
28
|
-
if (event.targetId.startsWith(
|
|
29
|
-
return { target_type:
|
|
50
|
+
if (event.type === 'schedule' || event.targetId.startsWith('sch_')) {
|
|
51
|
+
return { target_type: 'schedule', target_id: event.targetId };
|
|
30
52
|
}
|
|
31
|
-
if (event.type ===
|
|
32
|
-
|
|
53
|
+
if (event.type === 'wiki_comment') {
|
|
54
|
+
// target_id is the full wiki target_uri (scheme-stripped routing key). The
|
|
55
|
+
// server stores target_type freely and only publishes step WS events /
|
|
56
|
+
// projects for chat & task, so 'wiki' is an informational tag — no inline
|
|
57
|
+
// wiki step viewer exists yet.
|
|
58
|
+
return { target_type: 'wiki', target_id: event.targetId || undefined };
|
|
33
59
|
}
|
|
34
|
-
return { target_type:
|
|
60
|
+
return { target_type: '', target_id: event.targetId || undefined };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Minimal parse of a wiki / changeset comment `target_uri` into its routing key
|
|
64
|
+
* and a human label, without depending on `@parall/app`'s full `prll://` parser
|
|
65
|
+
* (agent-core only depends on `@parall/sdk`). Forms:
|
|
66
|
+
* prll://wik_abc/path/to/file.md[?rev=SHA#h=...] → wiki page / inline anchor
|
|
67
|
+
* prll://wcs_xyz?wiki=wik_abc → changeset
|
|
68
|
+
*
|
|
69
|
+
* `routingKey` is the full `target_uri` minus the `prll://` scheme — it is the
|
|
70
|
+
* gateway routing/serialization key, so distinct pages / inline anchors /
|
|
71
|
+
* changesets within the same wiki route as distinct conversations (using the
|
|
72
|
+
* bare `wik_`/`wcs_` entity id would collapse every comment in a wiki onto one
|
|
73
|
+
* lane). Stripping the scheme keeps `prll://${targetId}` correct everywhere it
|
|
74
|
+
* is reconstructed (event body, fork-scope prefix).
|
|
75
|
+
*/
|
|
76
|
+
function parseWikiCommentTarget(targetUri) {
|
|
77
|
+
const routingKey = targetUri.replace(/^prll:\/\//, '');
|
|
78
|
+
const entityId = routingKey.match(/^([^/?#]+)/)?.[1] ?? routingKey;
|
|
79
|
+
const targetType = entityId.startsWith('wcs_') ? 'changeset' : 'wiki';
|
|
80
|
+
let label = entityId;
|
|
81
|
+
if (targetType === 'wiki') {
|
|
82
|
+
const pathMatch = routingKey.slice(entityId.length).match(/^\/([^?#]+)/);
|
|
83
|
+
if (pathMatch) {
|
|
84
|
+
try {
|
|
85
|
+
label = decodeURIComponent(pathMatch[1]);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
label = pathMatch[1];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { routingKey, label, targetType };
|
|
35
93
|
}
|
|
36
94
|
async function fetchAllChats(client, orgId, chatInfoMap) {
|
|
37
95
|
let cursor;
|
|
@@ -53,8 +111,6 @@ async function fetchAllChats(client, orgId, chatInfoMap) {
|
|
|
53
111
|
export class ParallAgentGateway {
|
|
54
112
|
opts;
|
|
55
113
|
chatInfoMap = new Map();
|
|
56
|
-
activeDispatches = new Map();
|
|
57
|
-
injectedTypingCounts = new Map();
|
|
58
114
|
dispatchedTasks = new Set();
|
|
59
115
|
dispatchedMessages = new Set();
|
|
60
116
|
forkStates = new Map();
|
|
@@ -64,11 +120,10 @@ export class ParallAgentGateway {
|
|
|
64
120
|
pendingForkResults: [],
|
|
65
121
|
mainBuffer: [],
|
|
66
122
|
};
|
|
67
|
-
sessionId =
|
|
123
|
+
sessionId = '';
|
|
68
124
|
activeSessionId;
|
|
69
125
|
sessionBindings = new Map();
|
|
70
126
|
heartbeatTimer = null;
|
|
71
|
-
hadSuccessfulHello = false;
|
|
72
127
|
lastHeartbeatAt = Date.now();
|
|
73
128
|
draining = false;
|
|
74
129
|
// Graceful shutdown state. When SIGTERM / abort fires, `shuttingDown` flips
|
|
@@ -80,25 +135,30 @@ export class ParallAgentGateway {
|
|
|
80
135
|
drainResolvers = [];
|
|
81
136
|
pendingRestartNotification = null;
|
|
82
137
|
DISPATCHED_MESSAGES_CAP = 5000;
|
|
83
|
-
COLD_START_WINDOW_MS;
|
|
84
138
|
// SHUTDOWN_DEADLINE_MS is read by waitForDrain via the configured value
|
|
85
139
|
// below — kept as instance state so per-runtime configs can override it
|
|
86
140
|
// (see parseShutdownDeadlineMs and runtime entrypoints).
|
|
87
141
|
SHUTDOWN_DEADLINE_MS;
|
|
142
|
+
FORK_DEADLINE_MS;
|
|
143
|
+
DISPATCH_DEADLINE_MS;
|
|
88
144
|
constructor(opts) {
|
|
89
145
|
this.opts = opts;
|
|
90
|
-
this.COLD_START_WINDOW_MS = opts.coldStartWindowMs ?? 5 * 60_000;
|
|
91
146
|
this.SHUTDOWN_DEADLINE_MS = opts.shutdownDeadlineMs ?? 60_000;
|
|
147
|
+
this.FORK_DEADLINE_MS = opts.forkDeadlineMs ?? 2 * 60 * 60_000;
|
|
148
|
+
this.DISPATCH_DEADLINE_MS = opts.dispatchDeadlineMs ?? 20 * 60_000;
|
|
149
|
+
if (opts.coldStartWindowMs != null) {
|
|
150
|
+
opts.log?.warn?.('coldStartWindowMs is deprecated and ignored — cold-start time filter has been removed');
|
|
151
|
+
}
|
|
92
152
|
}
|
|
93
153
|
async run(abortSignal) {
|
|
94
154
|
const { ws, log } = this.opts;
|
|
95
155
|
ws.onStateChange((state) => {
|
|
96
156
|
log?.info(`connection state → ${state}`);
|
|
97
157
|
});
|
|
98
|
-
ws.on(
|
|
158
|
+
ws.on('hello', async (data) => {
|
|
99
159
|
await this.handleHello(data);
|
|
100
160
|
});
|
|
101
|
-
ws.on(
|
|
161
|
+
ws.on('chat.update', (data) => {
|
|
102
162
|
const changes = data.changes;
|
|
103
163
|
if (!changes)
|
|
104
164
|
return;
|
|
@@ -106,27 +166,27 @@ export class ParallAgentGateway {
|
|
|
106
166
|
if (existing) {
|
|
107
167
|
this.chatInfoMap.set(data.chat_id, {
|
|
108
168
|
...existing,
|
|
109
|
-
...(typeof changes.type ===
|
|
110
|
-
...(typeof changes.name ===
|
|
111
|
-
...(typeof changes.agent_routing_mode ===
|
|
169
|
+
...(typeof changes.type === 'string' ? { type: changes.type } : {}),
|
|
170
|
+
...(typeof changes.name === 'string' ? { name: changes.name } : {}),
|
|
171
|
+
...(typeof changes.agent_routing_mode === 'string'
|
|
112
172
|
? { agentRoutingMode: changes.agent_routing_mode }
|
|
113
173
|
: {}),
|
|
114
174
|
});
|
|
115
175
|
}
|
|
116
|
-
else if (typeof changes.type ===
|
|
176
|
+
else if (typeof changes.type === 'string') {
|
|
117
177
|
this.chatInfoMap.set(data.chat_id, {
|
|
118
178
|
type: changes.type,
|
|
119
|
-
name: typeof changes.name ===
|
|
120
|
-
agentRoutingMode: (typeof changes.agent_routing_mode ===
|
|
179
|
+
name: typeof changes.name === 'string' ? changes.name : null,
|
|
180
|
+
agentRoutingMode: (typeof changes.agent_routing_mode === 'string'
|
|
121
181
|
? changes.agent_routing_mode
|
|
122
|
-
:
|
|
182
|
+
: 'passive'),
|
|
123
183
|
});
|
|
124
184
|
}
|
|
125
185
|
});
|
|
126
|
-
ws.on(
|
|
186
|
+
ws.on('message.new', async (data) => {
|
|
127
187
|
await this.handleMessage(data);
|
|
128
188
|
});
|
|
129
|
-
ws.on(
|
|
189
|
+
ws.on('agent_config.update', async (data) => {
|
|
130
190
|
this.opts.log?.info(`config update notification (version=${data.version})`);
|
|
131
191
|
try {
|
|
132
192
|
await this.opts.onConfigUpdate?.(data);
|
|
@@ -135,13 +195,12 @@ export class ParallAgentGateway {
|
|
|
135
195
|
this.opts.log?.warn(`config update failed: ${String(err)}`);
|
|
136
196
|
}
|
|
137
197
|
});
|
|
138
|
-
ws.on(
|
|
139
|
-
const prevId = data.previous_session_id ??
|
|
198
|
+
ws.on('agent.new_session', async (data) => {
|
|
199
|
+
const prevId = data.previous_session_id ?? '';
|
|
140
200
|
this.opts.log?.info(`new session signal received (previous=${prevId})`);
|
|
141
201
|
this.sessionBindings.clear();
|
|
142
202
|
if (prevId) {
|
|
143
|
-
this.pendingRestartNotification =
|
|
144
|
-
`[Harness Notification] This is a fresh session. Your previous session (${prevId}) was ended by the user and you have been restarted.`;
|
|
203
|
+
this.pendingRestartNotification = `[Harness Notification] This is a fresh session. Your previous session (${prevId}) was ended by the user and you have been restarted.`;
|
|
145
204
|
}
|
|
146
205
|
try {
|
|
147
206
|
await this.opts.onNewSession?.(prevId);
|
|
@@ -150,27 +209,32 @@ export class ParallAgentGateway {
|
|
|
150
209
|
this.opts.log?.warn(`onNewSession callback failed: ${String(err)}`);
|
|
151
210
|
}
|
|
152
211
|
});
|
|
153
|
-
ws.on(
|
|
212
|
+
ws.on('recovery.overflow', () => {
|
|
154
213
|
this.opts.log?.warn(`recovery.overflow — triggering full catch-up`);
|
|
155
214
|
this.catchUpFromDispatch().catch((err) => this.opts.log?.warn(`overflow catch-up failed: ${String(err)}`));
|
|
156
215
|
});
|
|
157
|
-
ws.on(
|
|
216
|
+
ws.on('task.assigned', async (data) => {
|
|
158
217
|
if (data.assignee_id !== this.opts.agentUserId)
|
|
159
218
|
return;
|
|
160
|
-
if (data.status !==
|
|
219
|
+
if (data.status !== 'todo' && data.status !== 'in_progress')
|
|
161
220
|
return;
|
|
162
221
|
try {
|
|
163
222
|
const dispatched = await this.handleTaskAssignment(data, data.id);
|
|
164
223
|
if (dispatched) {
|
|
165
|
-
this.opts.client
|
|
224
|
+
this.opts.client
|
|
225
|
+
.ackDispatch(this.opts.config.org_id, {
|
|
226
|
+
source_type: 'task_activity',
|
|
227
|
+
source_id: data.id,
|
|
228
|
+
})
|
|
229
|
+
.catch(() => { });
|
|
166
230
|
}
|
|
167
231
|
}
|
|
168
232
|
catch (err) {
|
|
169
233
|
this.opts.log?.error(`task dispatch failed for ${data.id}: ${String(err)}`);
|
|
170
234
|
}
|
|
171
235
|
});
|
|
172
|
-
ws.on(
|
|
173
|
-
if (data.event_type ===
|
|
236
|
+
ws.on('dispatch.new', async (data) => {
|
|
237
|
+
if (data.event_type === 'task_comment') {
|
|
174
238
|
if (!data.source_id || !data.task_id)
|
|
175
239
|
return;
|
|
176
240
|
try {
|
|
@@ -183,7 +247,20 @@ export class ParallAgentGateway {
|
|
|
183
247
|
this.opts.log?.error(`task comment dispatch failed for ${data.source_id}: ${String(err)}`);
|
|
184
248
|
}
|
|
185
249
|
}
|
|
186
|
-
else if (data.event_type ===
|
|
250
|
+
else if (data.event_type === 'wiki_comment') {
|
|
251
|
+
if (!data.source_id)
|
|
252
|
+
return;
|
|
253
|
+
try {
|
|
254
|
+
const dispatched = await this.handleWikiComment(data.source_id, data.actor_id, data.delivery_reason);
|
|
255
|
+
if (dispatched) {
|
|
256
|
+
this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => { });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
this.opts.log?.error(`wiki comment dispatch failed for ${data.source_id}: ${String(err)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else if (data.event_type === 'task_update') {
|
|
187
264
|
if (!data.task_id)
|
|
188
265
|
return;
|
|
189
266
|
try {
|
|
@@ -196,7 +273,7 @@ export class ParallAgentGateway {
|
|
|
196
273
|
this.opts.log?.error(`task update dispatch failed for ${data.task_id}: ${String(err)}`);
|
|
197
274
|
}
|
|
198
275
|
}
|
|
199
|
-
else if (data.event_type ===
|
|
276
|
+
else if (data.event_type === 'schedule.fire') {
|
|
200
277
|
if (!data.source_id)
|
|
201
278
|
return;
|
|
202
279
|
try {
|
|
@@ -209,7 +286,7 @@ export class ParallAgentGateway {
|
|
|
209
286
|
this.opts.log?.error(`schedule fire dispatch failed for ${data.source_id}: ${String(err)}`);
|
|
210
287
|
}
|
|
211
288
|
}
|
|
212
|
-
else if (data.event_type ===
|
|
289
|
+
else if (data.event_type === 'approval_decided') {
|
|
213
290
|
if (!data.source_id)
|
|
214
291
|
return;
|
|
215
292
|
try {
|
|
@@ -222,7 +299,7 @@ export class ParallAgentGateway {
|
|
|
222
299
|
this.opts.log?.error(`approval decided dispatch failed for ${data.source_id}: ${String(err)}`);
|
|
223
300
|
}
|
|
224
301
|
}
|
|
225
|
-
else if (data.event_type !==
|
|
302
|
+
else if (data.event_type !== 'message' && data.event_type !== 'task_assign') {
|
|
226
303
|
// Truly unknown event_type — log so a newly-added dispatch type
|
|
227
304
|
// not yet wired here surfaces during runtime testing. "message"
|
|
228
305
|
// and "task_assign" are deliberately excluded: dispatch.new
|
|
@@ -233,10 +310,10 @@ export class ParallAgentGateway {
|
|
|
233
310
|
this.opts.log?.info(`dispatch.new with unhandled event_type=${String(data.event_type)} (id=${data.id}) — no-op`);
|
|
234
311
|
}
|
|
235
312
|
});
|
|
236
|
-
this.opts.log?.info(`connecting to ${this.opts.connectionLabel ??
|
|
313
|
+
this.opts.log?.info(`connecting to ${this.opts.connectionLabel ?? 'Parall WS'}...`);
|
|
237
314
|
await ws.connect();
|
|
238
315
|
return new Promise((resolve) => {
|
|
239
|
-
abortSignal.addEventListener(
|
|
316
|
+
abortSignal.addEventListener('abort', async () => {
|
|
240
317
|
await this.shutdown();
|
|
241
318
|
resolve();
|
|
242
319
|
});
|
|
@@ -256,60 +333,13 @@ export class ParallAgentGateway {
|
|
|
256
333
|
this.dispatchedMessages.add(id);
|
|
257
334
|
return true;
|
|
258
335
|
}
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.opts.ws.sendTyping(chatId, "start");
|
|
267
|
-
const typingRefresh = setInterval(() => {
|
|
268
|
-
if (this.opts.ws.state === "connected")
|
|
269
|
-
this.opts.ws.sendTyping(chatId, "start");
|
|
270
|
-
}, 2000);
|
|
271
|
-
this.activeDispatches.set(chatId, { count: 1, typingTimer: typingRefresh });
|
|
272
|
-
}
|
|
273
|
-
stopTyping(chatId) {
|
|
274
|
-
const dispatch = this.activeDispatches.get(chatId);
|
|
275
|
-
if (!dispatch)
|
|
276
|
-
return;
|
|
277
|
-
dispatch.count--;
|
|
278
|
-
if (dispatch.count > 0)
|
|
279
|
-
return;
|
|
280
|
-
clearInterval(dispatch.typingTimer);
|
|
281
|
-
this.activeDispatches.delete(chatId);
|
|
282
|
-
if (this.opts.ws.state === "connected")
|
|
283
|
-
this.opts.ws.sendTyping(chatId, "stop");
|
|
284
|
-
}
|
|
285
|
-
shouldShowTyping(event) {
|
|
286
|
-
return event.type === "message" && event.targetId.startsWith("cht_") && !event.noReply;
|
|
287
|
-
}
|
|
288
|
-
startInjectedTyping(event) {
|
|
289
|
-
if (!this.shouldShowTyping(event))
|
|
290
|
-
return;
|
|
291
|
-
this.startTyping(event.targetId);
|
|
292
|
-
this.injectedTypingCounts.set(event.targetId, (this.injectedTypingCounts.get(event.targetId) ?? 0) + 1);
|
|
293
|
-
}
|
|
294
|
-
takeInjectedTypingCount(chatId) {
|
|
295
|
-
const count = this.injectedTypingCounts.get(chatId) ?? 0;
|
|
296
|
-
this.injectedTypingCounts.delete(chatId);
|
|
297
|
-
return count;
|
|
298
|
-
}
|
|
299
|
-
async runDispatchWithTyping(event, sessionKey, bodyForAgent, earlierEvents = [], captureText, opts = {}) {
|
|
300
|
-
const showTyping = !opts.suppressStart && (this.shouldShowTyping(event) || earlierEvents.some(e => this.shouldShowTyping(e)));
|
|
301
|
-
if (showTyping)
|
|
302
|
-
this.startTyping(event.targetId);
|
|
303
|
-
try {
|
|
304
|
-
return await this.runDispatch(event, sessionKey, bodyForAgent, earlierEvents, captureText);
|
|
305
|
-
}
|
|
306
|
-
finally {
|
|
307
|
-
if (showTyping)
|
|
308
|
-
this.stopTyping(event.targetId);
|
|
309
|
-
for (let i = 0; i < (opts.injectedTypingCount ?? 0); i++) {
|
|
310
|
-
this.stopTyping(event.targetId);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
336
|
+
async emitDispatchReceived(event) {
|
|
337
|
+
const sourceType = event.ackSourceType ?? (event.type === 'task' ? 'task_activity' : 'message');
|
|
338
|
+
const sourceId = event.ackSourceId ?? event.messageId;
|
|
339
|
+
await this.opts.client.markDispatchReceived(this.opts.config.org_id, {
|
|
340
|
+
source_type: sourceType,
|
|
341
|
+
source_id: sourceId,
|
|
342
|
+
});
|
|
313
343
|
}
|
|
314
344
|
buildDispatchContext(event, sessionKey) {
|
|
315
345
|
const binding = this.sessionBindings.get(sessionKey);
|
|
@@ -322,7 +352,7 @@ export class ParallAgentGateway {
|
|
|
322
352
|
runtimeType: this.opts.runtimeType,
|
|
323
353
|
runtimeKey: this.opts.runtimeKey,
|
|
324
354
|
sessionId: binding?.agentSessionId,
|
|
325
|
-
chatId:
|
|
355
|
+
chatId: event.type === 'message' || event.type === 'approval' ? event.targetId : undefined,
|
|
326
356
|
triggerMessageId: event.messageId,
|
|
327
357
|
noReply: event.noReply ?? false,
|
|
328
358
|
contextFilePath: this.opts.contextFilePathForSession?.(sessionKey),
|
|
@@ -331,24 +361,41 @@ export class ParallAgentGateway {
|
|
|
331
361
|
log: this.opts.log,
|
|
332
362
|
};
|
|
333
363
|
}
|
|
364
|
+
isSessionNotLiveError(err) {
|
|
365
|
+
return (err instanceof ApiError &&
|
|
366
|
+
err.status === 409 &&
|
|
367
|
+
(err.code === 'SESSION_NOT_LIVE' || err.code === 'INVALID_TRANSITION'));
|
|
368
|
+
}
|
|
334
369
|
async createInputStep(sessionId, event) {
|
|
335
370
|
const target = resolveStepTarget(event);
|
|
336
371
|
try {
|
|
337
372
|
await this.opts.client.createAgentStep(this.opts.config.org_id, this.opts.agentUserId, sessionId, {
|
|
338
|
-
step_type:
|
|
373
|
+
step_type: 'input',
|
|
339
374
|
target_type: target.target_type,
|
|
340
375
|
target_id: target.target_id,
|
|
341
376
|
content: {
|
|
342
|
-
trigger_type: event.type ===
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
377
|
+
trigger_type: event.type === 'task'
|
|
378
|
+
? 'task_assign'
|
|
379
|
+
: event.type === 'task_comment'
|
|
380
|
+
? 'task_comment'
|
|
381
|
+
: event.type === 'wiki_comment'
|
|
382
|
+
? 'wiki_comment'
|
|
383
|
+
: event.type === 'schedule'
|
|
384
|
+
? 'schedule_fire'
|
|
385
|
+
: event.type === 'approval'
|
|
386
|
+
? 'approval_decided'
|
|
387
|
+
: 'mention',
|
|
388
|
+
trigger_ref: event.type === 'task'
|
|
389
|
+
? { task_id: event.targetId }
|
|
390
|
+
: event.type === 'task_comment'
|
|
391
|
+
? { comment_id: event.messageId, task_id: event.targetId }
|
|
392
|
+
: event.type === 'wiki_comment'
|
|
393
|
+
? { comment_id: event.messageId, target_uri: event.replyTargetUri }
|
|
394
|
+
: event.type === 'schedule'
|
|
395
|
+
? { schedule_id: event.targetId, run_id: event.messageId }
|
|
396
|
+
: event.type === 'approval'
|
|
397
|
+
? { approval_id: event.messageId }
|
|
398
|
+
: { message_id: event.messageId },
|
|
352
399
|
sender_id: event.senderId,
|
|
353
400
|
sender_name: event.senderName,
|
|
354
401
|
summary: event.body.substring(0, 200),
|
|
@@ -357,6 +404,8 @@ export class ParallAgentGateway {
|
|
|
357
404
|
});
|
|
358
405
|
}
|
|
359
406
|
catch (err) {
|
|
407
|
+
if (this.isSessionNotLiveError(err))
|
|
408
|
+
throw err;
|
|
360
409
|
this.opts.log?.warn(`failed to create input step: ${String(err)}`);
|
|
361
410
|
}
|
|
362
411
|
}
|
|
@@ -364,18 +413,18 @@ export class ParallAgentGateway {
|
|
|
364
413
|
const target = resolveStepTarget(event);
|
|
365
414
|
try {
|
|
366
415
|
switch (runtimeEvent.type) {
|
|
367
|
-
case
|
|
416
|
+
case 'thinking':
|
|
368
417
|
await this.opts.client.createAgentStep(this.opts.config.org_id, this.opts.agentUserId, sessionId, {
|
|
369
|
-
step_type:
|
|
418
|
+
step_type: 'thinking',
|
|
370
419
|
target_type: target.target_type,
|
|
371
420
|
target_id: target.target_id,
|
|
372
421
|
content: { text: runtimeEvent.text },
|
|
373
422
|
group_key: runtimeEvent.groupKey,
|
|
374
423
|
});
|
|
375
424
|
break;
|
|
376
|
-
case
|
|
425
|
+
case 'text':
|
|
377
426
|
await this.opts.client.createAgentStep(this.opts.config.org_id, this.opts.agentUserId, sessionId, {
|
|
378
|
-
step_type:
|
|
427
|
+
step_type: 'text',
|
|
379
428
|
target_type: target.target_type,
|
|
380
429
|
target_id: target.target_id,
|
|
381
430
|
content: {
|
|
@@ -386,16 +435,16 @@ export class ParallAgentGateway {
|
|
|
386
435
|
group_key: runtimeEvent.groupKey,
|
|
387
436
|
});
|
|
388
437
|
break;
|
|
389
|
-
case
|
|
438
|
+
case 'tool_call': {
|
|
390
439
|
const step = await this.opts.client.createAgentStep(this.opts.config.org_id, this.opts.agentUserId, sessionId, {
|
|
391
|
-
step_type:
|
|
440
|
+
step_type: 'tool_call',
|
|
392
441
|
target_type: target.target_type,
|
|
393
442
|
target_id: target.target_id,
|
|
394
443
|
content: {
|
|
395
444
|
call_id: runtimeEvent.callId,
|
|
396
445
|
tool_name: runtimeEvent.toolName,
|
|
397
446
|
tool_input: runtimeEvent.input,
|
|
398
|
-
status:
|
|
447
|
+
status: 'running',
|
|
399
448
|
started_at: runtimeEvent.startedAt ?? new Date().toISOString(),
|
|
400
449
|
},
|
|
401
450
|
group_key: runtimeEvent.groupKey,
|
|
@@ -409,15 +458,15 @@ export class ParallAgentGateway {
|
|
|
409
458
|
}
|
|
410
459
|
break;
|
|
411
460
|
}
|
|
412
|
-
case
|
|
461
|
+
case 'tool_result':
|
|
413
462
|
await this.opts.client.createAgentStep(this.opts.config.org_id, this.opts.agentUserId, sessionId, {
|
|
414
|
-
step_type:
|
|
463
|
+
step_type: 'tool_result',
|
|
415
464
|
target_type: target.target_type,
|
|
416
465
|
target_id: target.target_id,
|
|
417
466
|
content: {
|
|
418
467
|
call_id: runtimeEvent.callId,
|
|
419
468
|
tool_name: runtimeEvent.toolName,
|
|
420
|
-
status: runtimeEvent.error ?
|
|
469
|
+
status: runtimeEvent.error ? 'error' : 'success',
|
|
421
470
|
output: runtimeEvent.output,
|
|
422
471
|
duration_ms: runtimeEvent.durationMs ?? 0,
|
|
423
472
|
collapsible: true,
|
|
@@ -431,9 +480,9 @@ export class ParallAgentGateway {
|
|
|
431
480
|
this.clearStepIdFile(stepIdFilePath);
|
|
432
481
|
}
|
|
433
482
|
break;
|
|
434
|
-
case
|
|
483
|
+
case 'error':
|
|
435
484
|
await this.opts.client.createAgentStep(this.opts.config.org_id, this.opts.agentUserId, sessionId, {
|
|
436
|
-
step_type:
|
|
485
|
+
step_type: 'text',
|
|
437
486
|
target_type: target.target_type,
|
|
438
487
|
target_id: target.target_id,
|
|
439
488
|
content: { text: runtimeEvent.message, suppressed: false },
|
|
@@ -443,13 +492,15 @@ export class ParallAgentGateway {
|
|
|
443
492
|
}
|
|
444
493
|
}
|
|
445
494
|
catch (err) {
|
|
495
|
+
if (this.isSessionNotLiveError(err))
|
|
496
|
+
throw err;
|
|
446
497
|
this.opts.log?.warn(`failed to create ${runtimeEvent.type} step: ${String(err)}`);
|
|
447
498
|
}
|
|
448
499
|
}
|
|
449
500
|
writeContextFile(filePath, ctx) {
|
|
450
501
|
try {
|
|
451
502
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
452
|
-
fs.writeFileSync(filePath, JSON.stringify(ctx),
|
|
503
|
+
fs.writeFileSync(filePath, JSON.stringify(ctx), 'utf8');
|
|
453
504
|
}
|
|
454
505
|
catch (err) {
|
|
455
506
|
this.opts.log?.warn(`failed to write context file ${filePath}: ${String(err)}`);
|
|
@@ -457,10 +508,10 @@ export class ParallAgentGateway {
|
|
|
457
508
|
}
|
|
458
509
|
updateContextFileStepId(filePath, stepId) {
|
|
459
510
|
try {
|
|
460
|
-
const raw = fs.readFileSync(filePath,
|
|
511
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
461
512
|
const ctx = JSON.parse(raw);
|
|
462
513
|
ctx.step_id = stepId;
|
|
463
|
-
fs.writeFileSync(filePath, JSON.stringify(ctx),
|
|
514
|
+
fs.writeFileSync(filePath, JSON.stringify(ctx), 'utf8');
|
|
464
515
|
}
|
|
465
516
|
catch (err) {
|
|
466
517
|
this.opts.log?.warn(`failed to update context file step_id ${filePath}: ${String(err)}`);
|
|
@@ -468,10 +519,10 @@ export class ParallAgentGateway {
|
|
|
468
519
|
}
|
|
469
520
|
updateContextFileSessionId(filePath, sessionId) {
|
|
470
521
|
try {
|
|
471
|
-
const raw = fs.readFileSync(filePath,
|
|
522
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
472
523
|
const ctx = JSON.parse(raw);
|
|
473
524
|
ctx.session_id = sessionId;
|
|
474
|
-
fs.writeFileSync(filePath, JSON.stringify(ctx),
|
|
525
|
+
fs.writeFileSync(filePath, JSON.stringify(ctx), 'utf8');
|
|
475
526
|
}
|
|
476
527
|
catch (err) {
|
|
477
528
|
this.opts.log?.warn(`failed to update context file session_id ${filePath}: ${String(err)}`);
|
|
@@ -481,7 +532,7 @@ export class ParallAgentGateway {
|
|
|
481
532
|
writeStepIdFile(filePath, stepId) {
|
|
482
533
|
try {
|
|
483
534
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
484
|
-
fs.writeFileSync(filePath, stepId,
|
|
535
|
+
fs.writeFileSync(filePath, stepId, 'utf8');
|
|
485
536
|
}
|
|
486
537
|
catch (err) {
|
|
487
538
|
this.opts.log?.warn(`failed to write step id file ${filePath}: ${String(err)}`);
|
|
@@ -490,7 +541,7 @@ export class ParallAgentGateway {
|
|
|
490
541
|
/** @deprecated Use writeContextFile / updateContextFileStepId. */
|
|
491
542
|
clearStepIdFile(filePath) {
|
|
492
543
|
try {
|
|
493
|
-
fs.writeFileSync(filePath,
|
|
544
|
+
fs.writeFileSync(filePath, '', 'utf8');
|
|
494
545
|
}
|
|
495
546
|
catch {
|
|
496
547
|
// Best-effort cleanup.
|
|
@@ -524,6 +575,18 @@ export class ParallAgentGateway {
|
|
|
524
575
|
parent_session_id: parentSessionId,
|
|
525
576
|
runtime_ref: Object.keys(runtimeRef).length > 0 ? runtimeRef : undefined,
|
|
526
577
|
});
|
|
578
|
+
if (!LIVE_SESSION_STATUSES.has(session.status)) {
|
|
579
|
+
this.opts.log?.warn?.(`createAgentSession returned terminal session ${session.id} (${session.status}), resetting runtime for ${sessionKey}`);
|
|
580
|
+
this.sessionBindings.delete(sessionKey);
|
|
581
|
+
try {
|
|
582
|
+
await this.opts.onSessionStale?.(sessionKey);
|
|
583
|
+
}
|
|
584
|
+
catch (e) {
|
|
585
|
+
this.opts.log?.warn?.(`onSessionStale failed: ${e}`);
|
|
586
|
+
}
|
|
587
|
+
this.opts.log?.info?.(`stale session self-heal complete for ${sessionKey} — next dispatch will create a fresh session`);
|
|
588
|
+
throw new Error(`Agent session ${session.id} is terminal (${session.status})`);
|
|
589
|
+
}
|
|
527
590
|
const binding = {
|
|
528
591
|
sessionKey,
|
|
529
592
|
agentSessionId: session.id,
|
|
@@ -554,118 +617,237 @@ export class ParallAgentGateway {
|
|
|
554
617
|
return false;
|
|
555
618
|
}
|
|
556
619
|
if (this.pendingRestartNotification) {
|
|
557
|
-
bodyForAgent = this.pendingRestartNotification +
|
|
620
|
+
bodyForAgent = this.pendingRestartNotification + '\n\n---\n\n' + bodyForAgent;
|
|
558
621
|
this.pendingRestartNotification = null;
|
|
559
622
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
623
|
+
resetDispatchMetrics(sessionKey);
|
|
624
|
+
return runWithSessionKey(sessionKey, async () => {
|
|
625
|
+
let dispatchSpan = null;
|
|
626
|
+
setSessionChatId(sessionKey, event.targetId);
|
|
627
|
+
setSessionMessageId(sessionKey, event.messageId);
|
|
628
|
+
setDispatchMessageId(sessionKey, event.messageId);
|
|
629
|
+
setDispatchNoReply(sessionKey, event.noReply ?? false);
|
|
630
|
+
const dispatchContext = this.buildDispatchContext(event, sessionKey);
|
|
631
|
+
const contextFilePath = dispatchContext.contextFilePath;
|
|
632
|
+
const stepIdFilePath = dispatchContext.stepIdFilePath;
|
|
633
|
+
if (contextFilePath) {
|
|
634
|
+
this.writeContextFile(contextFilePath, {
|
|
635
|
+
session_id: dispatchContext.sessionId ?? null,
|
|
636
|
+
chat_id: dispatchContext.chatId ?? null,
|
|
637
|
+
trigger_message_id: dispatchContext.triggerMessageId ?? null,
|
|
638
|
+
no_reply: dispatchContext.noReply,
|
|
639
|
+
step_id: null,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
// sync: no await between the shuttingDown check above and this increment
|
|
643
|
+
// — JS event loop is single-threaded, so shutdown() cannot interleave
|
|
644
|
+
// here and miss our in-flight count.
|
|
645
|
+
this.inFlightDispatches++;
|
|
646
|
+
const deadlineTimer = this.DISPATCH_DEADLINE_MS > 0
|
|
647
|
+
? setTimeout(() => {
|
|
648
|
+
this.opts.log?.warn(`dispatch deadline exceeded (${this.DISPATCH_DEADLINE_MS}ms) for ${event.messageId} on ${sessionKey}; aborting`);
|
|
649
|
+
try {
|
|
650
|
+
this.opts.dispatchAdapter.abortDispatch?.(sessionKey);
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
this.opts.log?.warn(`abortDispatch threw for ${sessionKey}: ${String(err)}`);
|
|
654
|
+
}
|
|
655
|
+
}, this.DISPATCH_DEADLINE_MS)
|
|
656
|
+
: null;
|
|
657
|
+
let binding = this.sessionBindings.get(sessionKey);
|
|
658
|
+
let inputStepsCreated = false;
|
|
659
|
+
let triggerMessageSet = false;
|
|
660
|
+
let dispatchError;
|
|
661
|
+
const pendingSendCallIds = new Set();
|
|
662
|
+
try {
|
|
663
|
+
dispatchSpan = startDispatchSpan(event, this.opts.runtimeType, sessionKey);
|
|
664
|
+
for await (const runtimeEvent of this.opts.dispatchAdapter.dispatch({
|
|
665
|
+
event,
|
|
666
|
+
earlierEvents,
|
|
667
|
+
bodyForAgent,
|
|
668
|
+
sessionKey,
|
|
669
|
+
context: dispatchContext,
|
|
670
|
+
})) {
|
|
671
|
+
if (runtimeEvent.type === 'runtime_session') {
|
|
672
|
+
binding = await this.bindRuntimeSession(sessionKey, runtimeEvent, contextFilePath);
|
|
673
|
+
if (!inputStepsCreated) {
|
|
674
|
+
// Persist input steps for "earlier events" (batched events that arrived
|
|
675
|
+
// while a dispatch was in flight) inside the in-flight window so a
|
|
676
|
+
// shutdown short-circuit BEFORE this point cannot leave orphan input
|
|
677
|
+
// steps that the replacement pod would duplicate on replay.
|
|
678
|
+
if (earlierEvents.length > 0) {
|
|
679
|
+
await this.createInputStepsForEarlierEvents(binding.agentSessionId, earlierEvents);
|
|
680
|
+
}
|
|
681
|
+
await this.createInputStep(binding.agentSessionId, event);
|
|
682
|
+
inputStepsCreated = true;
|
|
683
|
+
}
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (!binding) {
|
|
687
|
+
const detail = runtimeEvent.type === 'error' ? `: ${runtimeEvent.message}` : '';
|
|
688
|
+
throw new Error(`runtime emitted ${runtimeEvent.type} before runtime_session${detail}`);
|
|
689
|
+
}
|
|
690
|
+
if (!triggerMessageSet) {
|
|
691
|
+
triggerMessageSet = true;
|
|
692
|
+
this.opts.client
|
|
693
|
+
.updateAgentSession(this.opts.config.org_id, this.opts.agentUserId, binding.agentSessionId, { status: 'active', trigger_message_id: event.messageId })
|
|
694
|
+
.catch((err) => this.opts.log?.warn?.(`failed to set session active: ${err}`));
|
|
695
|
+
}
|
|
593
696
|
if (!inputStepsCreated) {
|
|
594
|
-
// Persist input steps for "earlier events" (batched events that arrived
|
|
595
|
-
// while a dispatch was in flight) inside the in-flight window so a
|
|
596
|
-
// shutdown short-circuit BEFORE this point cannot leave orphan input
|
|
597
|
-
// steps that the replacement pod would duplicate on replay.
|
|
598
697
|
if (earlierEvents.length > 0) {
|
|
599
698
|
await this.createInputStepsForEarlierEvents(binding.agentSessionId, earlierEvents);
|
|
600
699
|
}
|
|
601
700
|
await this.createInputStep(binding.agentSessionId, event);
|
|
602
701
|
inputStepsCreated = true;
|
|
603
702
|
}
|
|
604
|
-
|
|
703
|
+
if (captureText && runtimeEvent.type === 'text' && runtimeEvent.text) {
|
|
704
|
+
captureText.push(runtimeEvent.text);
|
|
705
|
+
}
|
|
706
|
+
if (runtimeEvent.type === 'text' && runtimeEvent.text.length > 0) {
|
|
707
|
+
recordDeliverText(sessionKey, runtimeEvent.text.length);
|
|
708
|
+
}
|
|
709
|
+
else if (runtimeEvent.type === 'tool_call') {
|
|
710
|
+
recordToolCall(sessionKey);
|
|
711
|
+
const cmd = extractShellCommand(runtimeEvent.input);
|
|
712
|
+
if (isParallSendCommand(cmd)) {
|
|
713
|
+
pendingSendCallIds.add(runtimeEvent.callId);
|
|
714
|
+
}
|
|
715
|
+
else if (isParallNoReplyCommand(cmd)) {
|
|
716
|
+
recordNoReply(sessionKey);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
else if (runtimeEvent.type === 'tool_result' &&
|
|
720
|
+
pendingSendCallIds.delete(runtimeEvent.callId)) {
|
|
721
|
+
recordMessageSend(sessionKey, !runtimeEvent.error);
|
|
722
|
+
}
|
|
723
|
+
await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath);
|
|
605
724
|
}
|
|
606
725
|
if (!binding) {
|
|
607
|
-
|
|
608
|
-
throw new Error(`runtime emitted ${runtimeEvent.type} before runtime_session${detail}`);
|
|
726
|
+
binding = this.sessionBindings.get(sessionKey);
|
|
609
727
|
}
|
|
610
|
-
if (!
|
|
611
|
-
|
|
612
|
-
this.opts.client.updateAgentSession(this.opts.config.org_id, this.opts.agentUserId, binding.agentSessionId, { status: "active", trigger_message_id: event.messageId }).catch((err) => this.opts.log?.warn?.(`failed to set session active: ${err}`));
|
|
728
|
+
if (!binding) {
|
|
729
|
+
throw new Error('runtime completed without runtime_session');
|
|
613
730
|
}
|
|
614
731
|
if (!inputStepsCreated) {
|
|
615
732
|
if (earlierEvents.length > 0) {
|
|
616
733
|
await this.createInputStepsForEarlierEvents(binding.agentSessionId, earlierEvents);
|
|
617
734
|
}
|
|
618
735
|
await this.createInputStep(binding.agentSessionId, event);
|
|
619
|
-
inputStepsCreated = true;
|
|
620
736
|
}
|
|
621
|
-
if (captureText && runtimeEvent.type === "text" && runtimeEvent.text) {
|
|
622
|
-
captureText.push(runtimeEvent.text);
|
|
623
|
-
}
|
|
624
|
-
await this.createRuntimeStep(binding.agentSessionId, event, runtimeEvent, stepIdFilePath, contextFilePath);
|
|
625
|
-
}
|
|
626
|
-
if (!binding) {
|
|
627
|
-
binding = this.sessionBindings.get(sessionKey);
|
|
628
|
-
}
|
|
629
|
-
if (!binding) {
|
|
630
|
-
throw new Error("runtime completed without runtime_session");
|
|
631
737
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
738
|
+
catch (err) {
|
|
739
|
+
dispatchError = err;
|
|
740
|
+
let staleDetected = this.isSessionNotLiveError(err);
|
|
741
|
+
if (!staleDetected && binding) {
|
|
742
|
+
try {
|
|
743
|
+
await this.createRuntimeStep(binding.agentSessionId, event, {
|
|
744
|
+
type: 'error',
|
|
745
|
+
message: `Dispatch failed: ${String(err)}`,
|
|
746
|
+
}, stepIdFilePath, contextFilePath);
|
|
747
|
+
}
|
|
748
|
+
catch (stepErr) {
|
|
749
|
+
if (this.isSessionNotLiveError(stepErr))
|
|
750
|
+
staleDetected = true;
|
|
751
|
+
}
|
|
635
752
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
clearSessionMessageId(sessionKey);
|
|
653
|
-
clearDispatchMessageId(sessionKey);
|
|
654
|
-
clearDispatchNoReply(sessionKey);
|
|
655
|
-
if (contextFilePath) {
|
|
656
|
-
this.updateContextFileStepId(contextFilePath, null);
|
|
657
|
-
}
|
|
658
|
-
else if (stepIdFilePath) {
|
|
659
|
-
this.clearStepIdFile(stepIdFilePath);
|
|
753
|
+
if (staleDetected && binding) {
|
|
754
|
+
this.opts.log?.warn?.(`session ${binding.agentSessionId} is stale (mid-dispatch), triggering recovery for ${sessionKey}`);
|
|
755
|
+
this.sessionBindings.delete(sessionKey);
|
|
756
|
+
if (sessionKey === this.opts.runtimeKey) {
|
|
757
|
+
this.activeSessionId = undefined;
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
await this.opts.onSessionStale?.(sessionKey);
|
|
761
|
+
}
|
|
762
|
+
catch (e) {
|
|
763
|
+
this.opts.log?.warn?.(`onSessionStale failed: ${e}`);
|
|
764
|
+
}
|
|
765
|
+
this.opts.log?.info?.(`stale session self-heal complete for ${sessionKey} — next dispatch will create a fresh session`);
|
|
766
|
+
binding = undefined;
|
|
767
|
+
}
|
|
768
|
+
throw err;
|
|
660
769
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
770
|
+
finally {
|
|
771
|
+
if (deadlineTimer)
|
|
772
|
+
clearTimeout(deadlineTimer);
|
|
773
|
+
const metricsSnapshot = getDispatchMetrics(sessionKey);
|
|
774
|
+
const durationMs = metricsSnapshot ? Date.now() - metricsSnapshot.started_at : 0;
|
|
775
|
+
endDispatchSpan(dispatchSpan, metricsSnapshot, dispatchError);
|
|
776
|
+
recordDispatchMetric(event, this.opts.runtimeType, durationMs);
|
|
777
|
+
if (metricsSnapshot &&
|
|
778
|
+
!dispatchError &&
|
|
779
|
+
event.type === 'message' &&
|
|
780
|
+
event.targetId?.startsWith('cht_') &&
|
|
781
|
+
!event.noReply &&
|
|
782
|
+
metricsSnapshot.deliver_text_chunks > 0 &&
|
|
783
|
+
metricsSnapshot.message_send_successes === 0 &&
|
|
784
|
+
!metricsSnapshot.no_reply_called) {
|
|
785
|
+
recordMissingReply(this.opts.runtimeType);
|
|
786
|
+
}
|
|
787
|
+
clearDispatchMetrics(sessionKey);
|
|
788
|
+
if (triggerMessageSet && binding) {
|
|
789
|
+
this.opts.client
|
|
790
|
+
.updateAgentSession(this.opts.config.org_id, this.opts.agentUserId, binding.agentSessionId, { status: 'idle' })
|
|
791
|
+
.catch((err) => this.opts.log?.warn?.(`failed to set session idle: ${err}`));
|
|
792
|
+
}
|
|
793
|
+
clearSessionMessageId(sessionKey);
|
|
794
|
+
clearDispatchMessageId(sessionKey);
|
|
795
|
+
clearDispatchNoReply(sessionKey);
|
|
796
|
+
if (contextFilePath) {
|
|
797
|
+
this.updateContextFileStepId(contextFilePath, null);
|
|
798
|
+
}
|
|
799
|
+
else if (stepIdFilePath) {
|
|
800
|
+
this.clearStepIdFile(stepIdFilePath);
|
|
801
|
+
}
|
|
802
|
+
this.inFlightDispatches--;
|
|
803
|
+
if (this.inFlightDispatches === 0 && this.drainResolvers.length > 0) {
|
|
804
|
+
const resolvers = this.drainResolvers.splice(0);
|
|
805
|
+
for (const resolve of resolvers)
|
|
806
|
+
resolve();
|
|
807
|
+
}
|
|
666
808
|
}
|
|
809
|
+
return true;
|
|
810
|
+
}); // runWithSessionKey
|
|
811
|
+
}
|
|
812
|
+
abortFork(targetId, reason) {
|
|
813
|
+
const forkState = this.forkStates.get(targetId);
|
|
814
|
+
if (!forkState)
|
|
815
|
+
return;
|
|
816
|
+
this.opts.log?.warn(`aborting fork for ${targetId}: ${reason}`);
|
|
817
|
+
forkState.deadlineExceeded = true;
|
|
818
|
+
if (forkState.deadlineTimer) {
|
|
819
|
+
clearTimeout(forkState.deadlineTimer);
|
|
820
|
+
forkState.deadlineTimer = null;
|
|
821
|
+
}
|
|
822
|
+
this.dispatchState.activeForks.delete(targetId);
|
|
823
|
+
this.forkStates.delete(targetId);
|
|
824
|
+
for (const item of forkState.queue.splice(0)) {
|
|
825
|
+
item.resolve(false);
|
|
826
|
+
}
|
|
827
|
+
if (this.opts.dispatchAdapter.cleanupFork) {
|
|
828
|
+
const forkBinding = this.sessionBindings.get(forkState.fork.sessionKey);
|
|
829
|
+
const cleanupOpts = {
|
|
830
|
+
fork: forkState.fork,
|
|
831
|
+
context: {
|
|
832
|
+
accountId: this.opts.accountId,
|
|
833
|
+
apiUrl: this.opts.config.parall_url,
|
|
834
|
+
apiKey: this.opts.config.api_key,
|
|
835
|
+
orgId: this.opts.config.org_id,
|
|
836
|
+
agentUserId: this.opts.agentUserId,
|
|
837
|
+
runtimeType: this.opts.runtimeType,
|
|
838
|
+
runtimeKey: this.opts.runtimeKey,
|
|
839
|
+
sessionId: forkBinding?.agentSessionId,
|
|
840
|
+
noReply: false,
|
|
841
|
+
client: this.opts.client,
|
|
842
|
+
log: this.opts.log,
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
void Promise.resolve()
|
|
846
|
+
.then(() => this.opts.dispatchAdapter.cleanupFork?.(cleanupOpts))
|
|
847
|
+
.catch((err) => {
|
|
848
|
+
this.opts.log?.warn(`abortFork cleanupFork error for ${targetId}: ${String(err)}`);
|
|
849
|
+
});
|
|
667
850
|
}
|
|
668
|
-
return true;
|
|
669
851
|
}
|
|
670
852
|
async runForkDrainLoop(fork) {
|
|
671
853
|
let lastCapturedText = [];
|
|
@@ -677,7 +859,7 @@ export class ParallAgentGateway {
|
|
|
677
859
|
// shuttingDown gate and inFlightDispatches counter, so a shutdown
|
|
678
860
|
// landing mid-batch cannot leave orphan steps that replay would
|
|
679
861
|
// duplicate.
|
|
680
|
-
if (this.shuttingDown) {
|
|
862
|
+
if (this.shuttingDown || fork.deadlineExceeded) {
|
|
681
863
|
for (const item of fork.queue.splice(0))
|
|
682
864
|
item.resolve(false);
|
|
683
865
|
break;
|
|
@@ -688,7 +870,7 @@ export class ParallAgentGateway {
|
|
|
688
870
|
const earlier = events.slice(0, -1);
|
|
689
871
|
try {
|
|
690
872
|
const batchText = [];
|
|
691
|
-
const dispatched = await this.
|
|
873
|
+
const dispatched = await this.runDispatch(last, fork.fork.sessionKey, buildForkScopePrefix(last) + buildEventBody(last), earlier, batchText);
|
|
692
874
|
if (!dispatched) {
|
|
693
875
|
// Shutdown short-circuit — resolve un-acked so the server requeues
|
|
694
876
|
// for the replacement pod and stop draining further items.
|
|
@@ -699,6 +881,11 @@ export class ParallAgentGateway {
|
|
|
699
881
|
}
|
|
700
882
|
if (batchText.length > 0)
|
|
701
883
|
lastCapturedText = batchText;
|
|
884
|
+
if (fork.deadlineExceeded) {
|
|
885
|
+
for (const item of items)
|
|
886
|
+
item.resolve(false);
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
702
889
|
fork.processedEvents.push(...events);
|
|
703
890
|
for (const item of items) {
|
|
704
891
|
item.resolve(true);
|
|
@@ -717,9 +904,13 @@ export class ParallAgentGateway {
|
|
|
717
904
|
}
|
|
718
905
|
}
|
|
719
906
|
finally {
|
|
907
|
+
if (fork.deadlineTimer) {
|
|
908
|
+
clearTimeout(fork.deadlineTimer);
|
|
909
|
+
fork.deadlineTimer = null;
|
|
910
|
+
}
|
|
720
911
|
if (fork.processedEvents.length > 0) {
|
|
721
912
|
const first = fork.processedEvents[0];
|
|
722
|
-
const agentSummary = lastCapturedText.join(
|
|
913
|
+
const agentSummary = lastCapturedText.join('').trim() || undefined;
|
|
723
914
|
let historyPath;
|
|
724
915
|
try {
|
|
725
916
|
historyPath = this.opts.dispatchAdapter.getSessionHistoryPath?.(fork.fork.sessionKey);
|
|
@@ -742,8 +933,12 @@ export class ParallAgentGateway {
|
|
|
742
933
|
historyPath,
|
|
743
934
|
});
|
|
744
935
|
}
|
|
745
|
-
this.forkStates.
|
|
746
|
-
|
|
936
|
+
if (this.forkStates.get(fork.targetId) === fork) {
|
|
937
|
+
this.forkStates.delete(fork.targetId);
|
|
938
|
+
}
|
|
939
|
+
if (this.dispatchState.activeForks.get(fork.targetId) === fork.fork.sessionKey) {
|
|
940
|
+
this.dispatchState.activeForks.delete(fork.targetId);
|
|
941
|
+
}
|
|
747
942
|
const forkBinding = this.sessionBindings.get(fork.fork.sessionKey);
|
|
748
943
|
if (this.opts.dispatchAdapter.cleanupFork) {
|
|
749
944
|
const cleanupOpts = {
|
|
@@ -762,10 +957,17 @@ export class ParallAgentGateway {
|
|
|
762
957
|
log: this.opts.log,
|
|
763
958
|
},
|
|
764
959
|
};
|
|
765
|
-
|
|
960
|
+
try {
|
|
961
|
+
await this.opts.dispatchAdapter.cleanupFork(cleanupOpts);
|
|
962
|
+
}
|
|
963
|
+
catch (err) {
|
|
964
|
+
this.opts.log?.warn(`fork cleanupFork error for ${fork.targetId}: ${String(err)}`);
|
|
965
|
+
}
|
|
766
966
|
}
|
|
767
967
|
if (forkBinding) {
|
|
768
|
-
this.opts.client
|
|
968
|
+
this.opts.client
|
|
969
|
+
.updateAgentSession(this.opts.config.org_id, this.opts.agentUserId, forkBinding.agentSessionId, { status: 'closed' })
|
|
970
|
+
.catch(() => { });
|
|
769
971
|
this.sessionBindings.delete(fork.fork.sessionKey);
|
|
770
972
|
}
|
|
771
973
|
}
|
|
@@ -792,13 +994,22 @@ export class ParallAgentGateway {
|
|
|
792
994
|
const event = events[events.length - 1];
|
|
793
995
|
const earlier = events.slice(0, -1);
|
|
794
996
|
const hasPendingInjections = this.opts.dispatchAdapter.hasPendingInjections?.(this.opts.runtimeKey) ?? false;
|
|
795
|
-
const
|
|
796
|
-
|
|
997
|
+
const pendingFork = hasPendingInjections
|
|
998
|
+
? []
|
|
999
|
+
: this.dispatchState.pendingForkResults.splice(0);
|
|
797
1000
|
const forkPrefix = buildForkResultPrefix(pendingFork);
|
|
798
1001
|
this.dispatchState.mainCurrentTargetId = event.targetId;
|
|
799
|
-
this.dispatchState.mainPreDispatchBranchPoint =
|
|
800
|
-
|
|
801
|
-
|
|
1002
|
+
this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
|
|
1003
|
+
try {
|
|
1004
|
+
await this.emitDispatchReceived(event);
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
this.opts.log?.warn?.(`mark-received failed for buffered dispatch, leaving unacked for retry: ${String(err)}`);
|
|
1008
|
+
this.dispatchState.mainBuffer.unshift(...events);
|
|
1009
|
+
this.dispatchState.pendingForkResults.unshift(...pendingFork);
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
const dispatched = await this.runDispatch(event, this.opts.runtimeKey, forkPrefix + buildEventBody(event), earlier);
|
|
802
1013
|
if (!dispatched) {
|
|
803
1014
|
// Shutdown: skip the ack so the server redelivers these buffered
|
|
804
1015
|
// events to the replacement pod via dispatch catch-up. Put both the
|
|
@@ -808,12 +1019,15 @@ export class ParallAgentGateway {
|
|
|
808
1019
|
break;
|
|
809
1020
|
}
|
|
810
1021
|
for (const bufferedEvent of events) {
|
|
811
|
-
const sourceType = bufferedEvent.ackSourceType ??
|
|
1022
|
+
const sourceType = bufferedEvent.ackSourceType ??
|
|
1023
|
+
(bufferedEvent.type === 'task' ? 'task_activity' : 'message');
|
|
812
1024
|
const sourceId = bufferedEvent.ackSourceId ?? bufferedEvent.messageId;
|
|
813
|
-
this.opts.client
|
|
1025
|
+
this.opts.client
|
|
1026
|
+
.ackDispatch(this.opts.config.org_id, {
|
|
814
1027
|
source_type: sourceType,
|
|
815
1028
|
source_id: sourceId,
|
|
816
|
-
})
|
|
1029
|
+
})
|
|
1030
|
+
.catch(() => { });
|
|
817
1031
|
}
|
|
818
1032
|
}
|
|
819
1033
|
}
|
|
@@ -822,12 +1036,22 @@ export class ParallAgentGateway {
|
|
|
822
1036
|
this.dispatchState.mainDispatching = false;
|
|
823
1037
|
this.dispatchState.mainCurrentTargetId = undefined;
|
|
824
1038
|
this.dispatchState.mainPreDispatchBranchPoint = undefined;
|
|
1039
|
+
if (!this.shuttingDown && this.dispatchState.mainBuffer.length > 0) {
|
|
1040
|
+
setTimeout(() => {
|
|
1041
|
+
if (!this.draining &&
|
|
1042
|
+
!this.dispatchState.mainDispatching &&
|
|
1043
|
+
this.dispatchState.mainBuffer.length > 0) {
|
|
1044
|
+
this.dispatchState.mainDispatching = true;
|
|
1045
|
+
void this.drainMainBuffer();
|
|
1046
|
+
}
|
|
1047
|
+
}, 5000);
|
|
1048
|
+
}
|
|
825
1049
|
}
|
|
826
1050
|
}
|
|
827
1051
|
async handleInboundEvent(event) {
|
|
828
1052
|
const disposition = routeTrigger(event, this.dispatchState);
|
|
829
1053
|
switch (disposition.action) {
|
|
830
|
-
case
|
|
1054
|
+
case 'main': {
|
|
831
1055
|
const pendingFork = this.dispatchState.pendingForkResults.splice(0);
|
|
832
1056
|
const forkPrefix = buildForkResultPrefix(pendingFork);
|
|
833
1057
|
this.dispatchState.mainDispatching = true;
|
|
@@ -835,11 +1059,21 @@ export class ParallAgentGateway {
|
|
|
835
1059
|
// Snapshot the on-disk branch point BEFORE runDispatch starts writing
|
|
836
1060
|
// to the session file. Fork sessions created while main is in-flight
|
|
837
1061
|
// use this to branch from the clean pre-dispatch state.
|
|
838
|
-
this.dispatchState.mainPreDispatchBranchPoint =
|
|
839
|
-
|
|
1062
|
+
this.dispatchState.mainPreDispatchBranchPoint = this.opts.dispatchAdapter.getBranchPoint?.(this.opts.runtimeKey);
|
|
1063
|
+
try {
|
|
1064
|
+
await this.emitDispatchReceived(event);
|
|
1065
|
+
}
|
|
1066
|
+
catch (err) {
|
|
1067
|
+
this.opts.log?.warn?.(`mark-received failed, leaving unacked for retry: ${String(err)}`);
|
|
1068
|
+
this.dispatchState.mainDispatching = false;
|
|
1069
|
+
this.dispatchState.mainCurrentTargetId = undefined;
|
|
1070
|
+
this.dispatchState.mainPreDispatchBranchPoint = undefined;
|
|
1071
|
+
this.dispatchState.pendingForkResults.unshift(...pendingFork);
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
840
1074
|
let dispatched = false;
|
|
841
1075
|
try {
|
|
842
|
-
dispatched = await this.
|
|
1076
|
+
dispatched = await this.runDispatch(event, this.opts.runtimeKey, forkPrefix + buildEventBody(event));
|
|
843
1077
|
if (!dispatched) {
|
|
844
1078
|
// Shutdown short-circuit — restore the fork results so a future
|
|
845
1079
|
// pod can replay them, and return false so handleMessage skips ack.
|
|
@@ -851,7 +1085,7 @@ export class ParallAgentGateway {
|
|
|
851
1085
|
}
|
|
852
1086
|
return dispatched;
|
|
853
1087
|
}
|
|
854
|
-
case
|
|
1088
|
+
case 'buffer-main': {
|
|
855
1089
|
if (this.shuttingDown) {
|
|
856
1090
|
return false;
|
|
857
1091
|
}
|
|
@@ -860,20 +1094,21 @@ export class ParallAgentGateway {
|
|
|
860
1094
|
// gap between the steer await and the push.
|
|
861
1095
|
this.dispatchState.mainBuffer.push(event);
|
|
862
1096
|
if (this.dispatchState.mainCurrentTargetId === event.targetId &&
|
|
863
|
-
await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event))) {
|
|
864
|
-
this.startInjectedTyping(event);
|
|
1097
|
+
(await this.opts.dispatchAdapter.enqueueDuringDispatch?.(this.opts.runtimeKey, buildEventBody(event)))) {
|
|
865
1098
|
this.opts.log?.info(`steer injected for ${event.messageId} (will drain for bookkeeping)`);
|
|
866
1099
|
}
|
|
867
1100
|
// If the main dispatch cycle ended while we awaited the steer RPC,
|
|
868
1101
|
// our event is buffered but no drain is in flight. Re-enter the
|
|
869
1102
|
// drain to process it. The draining guard prevents re-entry.
|
|
870
|
-
if (!this.dispatchState.mainDispatching &&
|
|
1103
|
+
if (!this.dispatchState.mainDispatching &&
|
|
1104
|
+
!this.draining &&
|
|
1105
|
+
this.dispatchState.mainBuffer.length > 0) {
|
|
871
1106
|
this.dispatchState.mainDispatching = true;
|
|
872
1107
|
void this.drainMainBuffer();
|
|
873
1108
|
}
|
|
874
1109
|
return false;
|
|
875
1110
|
}
|
|
876
|
-
case
|
|
1111
|
+
case 'buffer-fork': {
|
|
877
1112
|
const activeFork = this.forkStates.get(event.targetId);
|
|
878
1113
|
if (!activeFork) {
|
|
879
1114
|
this.dispatchState.mainBuffer.push(event);
|
|
@@ -883,11 +1118,19 @@ export class ParallAgentGateway {
|
|
|
883
1118
|
activeFork.queue.push({ event, resolve });
|
|
884
1119
|
});
|
|
885
1120
|
}
|
|
886
|
-
case
|
|
1121
|
+
case 'new-fork': {
|
|
887
1122
|
if (!this.opts.dispatchAdapter.forkSession) {
|
|
888
1123
|
this.dispatchState.mainBuffer.push(event);
|
|
889
1124
|
return false;
|
|
890
1125
|
}
|
|
1126
|
+
try {
|
|
1127
|
+
await this.emitDispatchReceived(event);
|
|
1128
|
+
}
|
|
1129
|
+
catch (err) {
|
|
1130
|
+
this.opts.log?.warn?.(`mark-received failed for fork dispatch, leaving unacked for retry: ${String(err)}`);
|
|
1131
|
+
this.dispatchState.mainBuffer.push(event);
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
891
1134
|
const fork = await this.opts.dispatchAdapter.forkSession({
|
|
892
1135
|
sessionKey: this.opts.runtimeKey,
|
|
893
1136
|
context: this.buildDispatchContext(event, this.opts.runtimeKey),
|
|
@@ -903,9 +1146,14 @@ export class ParallAgentGateway {
|
|
|
903
1146
|
targetId: event.targetId,
|
|
904
1147
|
queue: [],
|
|
905
1148
|
processedEvents: [],
|
|
1149
|
+
deadlineTimer: null,
|
|
1150
|
+
deadlineExceeded: false,
|
|
906
1151
|
};
|
|
907
1152
|
this.forkStates.set(event.targetId, activeFork);
|
|
908
1153
|
this.dispatchState.activeForks.set(event.targetId, fork.sessionKey);
|
|
1154
|
+
activeFork.deadlineTimer = setTimeout(() => {
|
|
1155
|
+
this.abortFork(event.targetId, `deadline exceeded (${this.FORK_DEADLINE_MS}ms)`);
|
|
1156
|
+
}, this.FORK_DEADLINE_MS);
|
|
909
1157
|
const firstEventPromise = new Promise((resolve) => {
|
|
910
1158
|
activeFork.queue.push({ event, resolve });
|
|
911
1159
|
});
|
|
@@ -936,22 +1184,22 @@ export class ParallAgentGateway {
|
|
|
936
1184
|
}
|
|
937
1185
|
}
|
|
938
1186
|
async buildMessageDispatchDecision(chatId, message) {
|
|
939
|
-
if (message.message_type !==
|
|
940
|
-
return { action:
|
|
1187
|
+
if (message.message_type !== 'text') {
|
|
1188
|
+
return { action: 'skip' };
|
|
941
1189
|
}
|
|
942
1190
|
const chatInfo = await this.getOrFetchChatInfo(chatId);
|
|
943
1191
|
if (!chatInfo)
|
|
944
|
-
return { action:
|
|
1192
|
+
return { action: 'retry' };
|
|
945
1193
|
const content = message.content;
|
|
946
|
-
const body = content.text?.trim() ??
|
|
1194
|
+
const body = content.text?.trim() ?? '';
|
|
947
1195
|
const hasAttachments = message.attachments?.length;
|
|
948
1196
|
if (!body && !hasAttachments)
|
|
949
|
-
return { action:
|
|
950
|
-
if (chatInfo.type ===
|
|
1197
|
+
return { action: 'skip' };
|
|
1198
|
+
if (chatInfo.type === 'group' && chatInfo.agentRoutingMode !== 'active') {
|
|
951
1199
|
const mentions = content.mentions ?? [];
|
|
952
1200
|
const isMentioned = mentions.some((mention) => mention.user_id === this.opts.agentUserId || mention.user_id === MENTION_ALL_USER_ID);
|
|
953
1201
|
if (!isMentioned)
|
|
954
|
-
return { action:
|
|
1202
|
+
return { action: 'skip' };
|
|
955
1203
|
}
|
|
956
1204
|
const attachments = (message.attachments ?? []).map((a) => ({
|
|
957
1205
|
id: a.id,
|
|
@@ -959,23 +1207,56 @@ export class ParallAgentGateway {
|
|
|
959
1207
|
fileSize: a.file_size,
|
|
960
1208
|
mimeType: a.mime_type,
|
|
961
1209
|
}));
|
|
1210
|
+
// Fetch unread context (best-effort, parallelized).
|
|
1211
|
+
let unreadCount;
|
|
1212
|
+
let unreadSince;
|
|
1213
|
+
let threadReplyCount;
|
|
1214
|
+
let threadUnreadCount;
|
|
1215
|
+
let threadUnreadSince;
|
|
1216
|
+
const isPassiveOrSmart = chatInfo.type === 'group' && chatInfo.agentRoutingMode !== 'active';
|
|
1217
|
+
const unreadPromise = isPassiveOrSmart
|
|
1218
|
+
? this.opts.client.getUnreadCounts(this.opts.config.org_id).catch(() => undefined)
|
|
1219
|
+
: Promise.resolve(undefined);
|
|
1220
|
+
const threadPromise = message.thread_root_id
|
|
1221
|
+
? this.opts.client
|
|
1222
|
+
.getThreadUnread(this.opts.config.org_id, chatId, message.thread_root_id)
|
|
1223
|
+
.catch(() => undefined)
|
|
1224
|
+
: Promise.resolve(undefined);
|
|
1225
|
+
const [counts, threadUnread] = await Promise.all([unreadPromise, threadPromise]);
|
|
1226
|
+
if (counts) {
|
|
1227
|
+
const entry = counts[chatId];
|
|
1228
|
+
if (entry && entry.count > 1) {
|
|
1229
|
+
unreadCount = entry.count;
|
|
1230
|
+
unreadSince = entry.since;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (threadUnread) {
|
|
1234
|
+
threadReplyCount = threadUnread.total;
|
|
1235
|
+
threadUnreadCount = threadUnread.unread > 0 ? threadUnread.unread : undefined;
|
|
1236
|
+
threadUnreadSince = threadUnread.since ?? undefined;
|
|
1237
|
+
}
|
|
962
1238
|
return {
|
|
963
|
-
action:
|
|
1239
|
+
action: 'dispatch',
|
|
964
1240
|
event: {
|
|
965
|
-
type:
|
|
1241
|
+
type: 'message',
|
|
966
1242
|
targetId: chatId,
|
|
967
1243
|
targetName: chatInfo.name ?? undefined,
|
|
968
1244
|
targetType: chatInfo.type,
|
|
969
1245
|
senderId: message.sender_id,
|
|
970
1246
|
senderName: message.sender?.display_name ?? message.sender_id,
|
|
971
1247
|
messageId: message.id,
|
|
972
|
-
body: body ||
|
|
1248
|
+
body: body || '[attachment]',
|
|
973
1249
|
threadRootId: message.thread_root_id ?? undefined,
|
|
974
1250
|
noReply: message.hints?.no_reply ?? false,
|
|
975
1251
|
attachments: attachments.length > 0 ? attachments : undefined,
|
|
976
1252
|
sentAt: message.created_at,
|
|
977
|
-
ackSourceType:
|
|
1253
|
+
ackSourceType: 'message',
|
|
978
1254
|
ackSourceId: message.id,
|
|
1255
|
+
unreadCount,
|
|
1256
|
+
unreadSince,
|
|
1257
|
+
threadReplyCount,
|
|
1258
|
+
threadUnreadCount,
|
|
1259
|
+
threadUnreadSince,
|
|
979
1260
|
},
|
|
980
1261
|
};
|
|
981
1262
|
}
|
|
@@ -985,12 +1266,12 @@ export class ParallAgentGateway {
|
|
|
985
1266
|
const chatId = data.chat_id;
|
|
986
1267
|
if (data.sender_id === this.opts.agentUserId)
|
|
987
1268
|
return;
|
|
988
|
-
if (data.message_type !==
|
|
1269
|
+
if (data.message_type !== 'text')
|
|
989
1270
|
return;
|
|
990
1271
|
if (!this.tryClaimMessage(data.id))
|
|
991
1272
|
return;
|
|
992
1273
|
const decision = await this.buildMessageDispatchDecision(chatId, data);
|
|
993
|
-
if (decision.action !==
|
|
1274
|
+
if (decision.action !== 'dispatch') {
|
|
994
1275
|
this.dispatchedMessages.delete(data.id);
|
|
995
1276
|
return;
|
|
996
1277
|
}
|
|
@@ -998,7 +1279,9 @@ export class ParallAgentGateway {
|
|
|
998
1279
|
try {
|
|
999
1280
|
const dispatched = await this.handleInboundEvent(event);
|
|
1000
1281
|
if (dispatched) {
|
|
1001
|
-
this.opts.client
|
|
1282
|
+
this.opts.client
|
|
1283
|
+
.ackDispatch(this.opts.config.org_id, { source_type: 'message', source_id: data.id })
|
|
1284
|
+
.catch(() => { });
|
|
1002
1285
|
}
|
|
1003
1286
|
else {
|
|
1004
1287
|
this.dispatchedMessages.delete(data.id);
|
|
@@ -1026,18 +1309,18 @@ export class ParallAgentGateway {
|
|
|
1026
1309
|
if (task.parent_id)
|
|
1027
1310
|
parts.push(`Parent: prll://${task.parent_id}`);
|
|
1028
1311
|
if (task.description)
|
|
1029
|
-
parts.push(
|
|
1312
|
+
parts.push('', task.description);
|
|
1030
1313
|
const event = {
|
|
1031
|
-
type:
|
|
1314
|
+
type: 'task',
|
|
1032
1315
|
targetId: task.id,
|
|
1033
1316
|
targetName: task.identifier ?? undefined,
|
|
1034
|
-
targetType:
|
|
1317
|
+
targetType: 'task',
|
|
1035
1318
|
senderId: task.creator_id,
|
|
1036
|
-
senderName:
|
|
1319
|
+
senderName: 'system',
|
|
1037
1320
|
messageId: task.id,
|
|
1038
|
-
body: parts.join(
|
|
1321
|
+
body: parts.join('\n'),
|
|
1039
1322
|
sentAt: task.updated_at ?? task.created_at,
|
|
1040
|
-
ackSourceType:
|
|
1323
|
+
ackSourceType: 'task_activity',
|
|
1041
1324
|
ackSourceId,
|
|
1042
1325
|
};
|
|
1043
1326
|
const dispatched = await this.handleInboundEvent(event);
|
|
@@ -1100,9 +1383,11 @@ export class ParallAgentGateway {
|
|
|
1100
1383
|
try {
|
|
1101
1384
|
task = await this.opts.client.getTask(this.opts.config.org_id, taskId);
|
|
1102
1385
|
}
|
|
1103
|
-
catch {
|
|
1386
|
+
catch {
|
|
1387
|
+
/* task context is optional */
|
|
1388
|
+
}
|
|
1104
1389
|
const taskLabel = task ? `${task.identifier ?? task.id} "${task.title}"` : taskId;
|
|
1105
|
-
this.opts.log?.info(`task comment on ${taskLabel} by ${actorId ??
|
|
1390
|
+
this.opts.log?.info(`task comment on ${taskLabel} by ${actorId ?? 'unknown'}`);
|
|
1106
1391
|
const parts = [];
|
|
1107
1392
|
if (task) {
|
|
1108
1393
|
parts.push(`Task: ${task.title} (prll://${task.id})`);
|
|
@@ -1111,19 +1396,87 @@ export class ParallAgentGateway {
|
|
|
1111
1396
|
else {
|
|
1112
1397
|
parts.push(`Task: prll://${taskId}`);
|
|
1113
1398
|
}
|
|
1114
|
-
parts.push(`Comment by: ${comment.author?.display_name ?? actorId ??
|
|
1115
|
-
parts.push(
|
|
1399
|
+
parts.push(`Comment by: ${comment.author?.display_name ?? actorId ?? 'unknown'} (prll://${comment.author_id})`);
|
|
1400
|
+
parts.push('', comment.body);
|
|
1116
1401
|
const event = {
|
|
1117
|
-
type:
|
|
1402
|
+
type: 'task_comment',
|
|
1118
1403
|
targetId: taskId,
|
|
1119
1404
|
targetName: task?.identifier ?? undefined,
|
|
1120
|
-
targetType:
|
|
1405
|
+
targetType: 'task',
|
|
1121
1406
|
senderId: comment.author_id,
|
|
1122
|
-
senderName: comment.author?.display_name ?? actorId ??
|
|
1407
|
+
senderName: comment.author?.display_name ?? actorId ?? 'unknown',
|
|
1123
1408
|
messageId: commentId,
|
|
1124
|
-
body: parts.join(
|
|
1409
|
+
body: parts.join('\n'),
|
|
1125
1410
|
deliveryReason: deliveryReason ?? undefined,
|
|
1126
|
-
ackSourceType:
|
|
1411
|
+
ackSourceType: 'comment',
|
|
1412
|
+
ackSourceId: commentId,
|
|
1413
|
+
};
|
|
1414
|
+
let dispatched;
|
|
1415
|
+
try {
|
|
1416
|
+
dispatched = await this.handleInboundEvent(event);
|
|
1417
|
+
}
|
|
1418
|
+
catch (err) {
|
|
1419
|
+
// Clear dedupe key so the event remains retryable on next catch-up.
|
|
1420
|
+
this.dispatchedTasks.delete(dedupeKey);
|
|
1421
|
+
throw err;
|
|
1422
|
+
}
|
|
1423
|
+
if (!dispatched) {
|
|
1424
|
+
this.dispatchedTasks.delete(dedupeKey);
|
|
1425
|
+
}
|
|
1426
|
+
return dispatched;
|
|
1427
|
+
}
|
|
1428
|
+
async handleWikiComment(commentId, actorId, deliveryReason) {
|
|
1429
|
+
if (this.shuttingDown)
|
|
1430
|
+
return false; // drain window — let server requeue via catch-up
|
|
1431
|
+
// Shares the comment dedupe namespace with handleTaskComment; comment IDs
|
|
1432
|
+
// are globally unique so wiki/task keys never collide.
|
|
1433
|
+
const dedupeKey = `comment:${commentId}`;
|
|
1434
|
+
if (this.dispatchedTasks.has(dedupeKey))
|
|
1435
|
+
return false;
|
|
1436
|
+
this.dispatchedTasks.add(dedupeKey);
|
|
1437
|
+
let comment = null;
|
|
1438
|
+
try {
|
|
1439
|
+
comment = await this.opts.client.getComment(this.opts.config.org_id, commentId);
|
|
1440
|
+
}
|
|
1441
|
+
catch (err) {
|
|
1442
|
+
const status = err?.status;
|
|
1443
|
+
// 404 (deleted) and 403 (this agent lacks wiki read access to the
|
|
1444
|
+
// target) are both permanent for this dispatch — ack the stale delivery
|
|
1445
|
+
// so it doesn't retry on every catch-up. Other errors are transient.
|
|
1446
|
+
if (status === 404 || status === 403) {
|
|
1447
|
+
this.opts.log?.info(`skipping inaccessible wiki comment ${commentId} (status ${status}), acking stale dispatch`);
|
|
1448
|
+
this.dispatchedTasks.delete(dedupeKey);
|
|
1449
|
+
return true; // caller will ack
|
|
1450
|
+
}
|
|
1451
|
+
this.dispatchedTasks.delete(dedupeKey);
|
|
1452
|
+
return false; // transient error — leave pending for retry
|
|
1453
|
+
}
|
|
1454
|
+
if (!comment) {
|
|
1455
|
+
this.dispatchedTasks.delete(dedupeKey);
|
|
1456
|
+
return true; // null response = gone, ack stale dispatch
|
|
1457
|
+
}
|
|
1458
|
+
if (comment.hints?.no_reply) {
|
|
1459
|
+
this.opts.log?.info(`skipping no_reply wiki comment ${commentId}, acking stale dispatch`);
|
|
1460
|
+
this.dispatchedTasks.delete(dedupeKey);
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1463
|
+
const target = parseWikiCommentTarget(comment.target_uri);
|
|
1464
|
+
this.opts.log?.info(`wiki comment on ${target.label} by ${actorId ?? 'unknown'}`);
|
|
1465
|
+
const event = {
|
|
1466
|
+
type: 'wiki_comment',
|
|
1467
|
+
// Full target_uri (scheme-stripped) is the routing key so different
|
|
1468
|
+
// pages / inline anchors / changesets in the same wiki don't collide on
|
|
1469
|
+
// one gateway lane. replyTargetUri keeps the canonical prll:// form.
|
|
1470
|
+
targetId: target.routingKey,
|
|
1471
|
+
targetName: target.label,
|
|
1472
|
+
targetType: target.targetType,
|
|
1473
|
+
senderId: comment.author_id,
|
|
1474
|
+
senderName: comment.author?.display_name ?? actorId ?? 'unknown',
|
|
1475
|
+
messageId: commentId,
|
|
1476
|
+
body: comment.body,
|
|
1477
|
+
deliveryReason: deliveryReason ?? undefined,
|
|
1478
|
+
replyTargetUri: comment.target_uri,
|
|
1479
|
+
ackSourceType: 'comment',
|
|
1127
1480
|
ackSourceId: commentId,
|
|
1128
1481
|
};
|
|
1129
1482
|
let dispatched;
|
|
@@ -1180,20 +1533,20 @@ export class ParallAgentGateway {
|
|
|
1180
1533
|
this.dispatchedTasks.add(dedupeKey);
|
|
1181
1534
|
this.opts.log?.info(`schedule fired: ${run.id} (schedule ${run.schedule_id})`);
|
|
1182
1535
|
const event = {
|
|
1183
|
-
type:
|
|
1536
|
+
type: 'schedule',
|
|
1184
1537
|
// Route by schedule_id (not attached chat_id) so concurrent fires of
|
|
1185
1538
|
// different schedules can fork independently — matches the PR1 primitive
|
|
1186
1539
|
// design where "schedule triggers; target decides response" and fire
|
|
1187
1540
|
// semantics are independent of any attached conversation.
|
|
1188
1541
|
targetId: run.schedule_id,
|
|
1189
|
-
targetType:
|
|
1190
|
-
senderId: actorId ??
|
|
1191
|
-
senderName:
|
|
1542
|
+
targetType: 'schedule',
|
|
1543
|
+
senderId: actorId ?? 'system',
|
|
1544
|
+
senderName: 'schedule',
|
|
1192
1545
|
messageId: run.id,
|
|
1193
|
-
body: run.fired_description ??
|
|
1546
|
+
body: run.fired_description ?? '',
|
|
1194
1547
|
scheduledFireAt: run.scheduled_fire_at,
|
|
1195
1548
|
attachedUri: run.fired_attached_uri ?? undefined,
|
|
1196
|
-
ackSourceType:
|
|
1549
|
+
ackSourceType: 'schedule_run',
|
|
1197
1550
|
ackSourceId: run.id,
|
|
1198
1551
|
};
|
|
1199
1552
|
let dispatched;
|
|
@@ -1232,14 +1585,14 @@ export class ParallAgentGateway {
|
|
|
1232
1585
|
return false;
|
|
1233
1586
|
this.dispatchedTasks.add(dedupeKey);
|
|
1234
1587
|
this.opts.log?.info(`approval decided: ${approval.id} (${approval.status})`);
|
|
1235
|
-
const statusLabel = approval.status ===
|
|
1236
|
-
const execInfo = approval.execution_status ? ` | execution: ${approval.execution_status}` :
|
|
1588
|
+
const statusLabel = approval.status === 'approved' ? 'Approved' : 'Rejected';
|
|
1589
|
+
const execInfo = approval.execution_status ? ` | execution: ${approval.execution_status}` : '';
|
|
1237
1590
|
const body = `${statusLabel}: ${approval.title}${execInfo}`;
|
|
1238
1591
|
const event = {
|
|
1239
|
-
type:
|
|
1592
|
+
type: 'approval',
|
|
1240
1593
|
targetId: chatId ?? approval.chat_id,
|
|
1241
|
-
senderId: actorId ?? approval.decided_by ??
|
|
1242
|
-
senderName:
|
|
1594
|
+
senderId: actorId ?? approval.decided_by ?? 'system',
|
|
1595
|
+
senderName: 'approver',
|
|
1243
1596
|
messageId: approval.id,
|
|
1244
1597
|
body,
|
|
1245
1598
|
};
|
|
@@ -1256,11 +1609,9 @@ export class ParallAgentGateway {
|
|
|
1256
1609
|
}
|
|
1257
1610
|
return dispatched;
|
|
1258
1611
|
}
|
|
1259
|
-
async catchUpFromDispatch(
|
|
1260
|
-
const minAge = coldStart ? Date.now() - this.COLD_START_WINDOW_MS : 0;
|
|
1612
|
+
async catchUpFromDispatch() {
|
|
1261
1613
|
let cursor;
|
|
1262
1614
|
let processed = 0;
|
|
1263
|
-
let skippedOld = 0;
|
|
1264
1615
|
do {
|
|
1265
1616
|
const page = await this.opts.client.getDispatch(this.opts.config.org_id, {
|
|
1266
1617
|
limit: 50,
|
|
@@ -1271,15 +1622,10 @@ export class ParallAgentGateway {
|
|
|
1271
1622
|
// will only reject. Items remain unacked for the replacement pod.
|
|
1272
1623
|
if (this.shuttingDown)
|
|
1273
1624
|
break;
|
|
1274
|
-
if (minAge > 0 && new Date(item.created_at).getTime() < minAge) {
|
|
1275
|
-
this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => { });
|
|
1276
|
-
skippedOld++;
|
|
1277
|
-
continue;
|
|
1278
|
-
}
|
|
1279
1625
|
processed++;
|
|
1280
1626
|
try {
|
|
1281
1627
|
let dispatched = false;
|
|
1282
|
-
if (item.event_type ===
|
|
1628
|
+
if (item.event_type === 'task_assign' && item.task_id) {
|
|
1283
1629
|
try {
|
|
1284
1630
|
dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id);
|
|
1285
1631
|
}
|
|
@@ -1288,7 +1634,7 @@ export class ParallAgentGateway {
|
|
|
1288
1634
|
continue;
|
|
1289
1635
|
}
|
|
1290
1636
|
}
|
|
1291
|
-
else if (item.event_type ===
|
|
1637
|
+
else if (item.event_type === 'task_update' && item.task_id) {
|
|
1292
1638
|
try {
|
|
1293
1639
|
dispatched = await this.handleTaskDispatch(item.task_id, item.source_id ?? item.task_id, { allowCreator: true });
|
|
1294
1640
|
}
|
|
@@ -1297,16 +1643,19 @@ export class ParallAgentGateway {
|
|
|
1297
1643
|
continue;
|
|
1298
1644
|
}
|
|
1299
1645
|
}
|
|
1300
|
-
else if (item.event_type ===
|
|
1646
|
+
else if (item.event_type === 'task_comment' && item.source_id && item.task_id) {
|
|
1301
1647
|
dispatched = await this.handleTaskComment(item.source_id, item.task_id, item.actor_id, item.delivery_reason);
|
|
1302
1648
|
}
|
|
1303
|
-
else if (item.event_type ===
|
|
1649
|
+
else if (item.event_type === 'wiki_comment' && item.source_id) {
|
|
1650
|
+
dispatched = await this.handleWikiComment(item.source_id, item.actor_id, item.delivery_reason);
|
|
1651
|
+
}
|
|
1652
|
+
else if (item.event_type === 'schedule.fire' && item.source_id) {
|
|
1304
1653
|
dispatched = await this.fetchAndHandleScheduleFire(item.source_id, item.actor_id);
|
|
1305
1654
|
}
|
|
1306
|
-
else if (item.event_type ===
|
|
1655
|
+
else if (item.event_type === 'approval_decided' && item.source_id) {
|
|
1307
1656
|
dispatched = await this.fetchAndHandleApprovalDecided(item.source_id, item.actor_id, item.chat_id ?? null);
|
|
1308
1657
|
}
|
|
1309
|
-
else if (item.event_type ===
|
|
1658
|
+
else if (item.event_type === 'message' && item.source_id && item.chat_id) {
|
|
1310
1659
|
if (!this.tryClaimMessage(item.source_id))
|
|
1311
1660
|
continue;
|
|
1312
1661
|
let msg = null;
|
|
@@ -1334,11 +1683,11 @@ export class ParallAgentGateway {
|
|
|
1334
1683
|
continue;
|
|
1335
1684
|
}
|
|
1336
1685
|
const decision = await this.buildMessageDispatchDecision(item.chat_id, msg);
|
|
1337
|
-
if (decision.action ===
|
|
1686
|
+
if (decision.action === 'retry') {
|
|
1338
1687
|
this.dispatchedMessages.delete(item.source_id);
|
|
1339
1688
|
continue;
|
|
1340
1689
|
}
|
|
1341
|
-
if (decision.action ===
|
|
1690
|
+
if (decision.action === 'skip') {
|
|
1342
1691
|
this.dispatchedMessages.delete(item.source_id);
|
|
1343
1692
|
this.opts.client.ackDispatchByID(this.opts.config.org_id, item.id).catch(() => { });
|
|
1344
1693
|
continue;
|
|
@@ -1357,13 +1706,20 @@ export class ParallAgentGateway {
|
|
|
1357
1706
|
// and the next page would be wasted API work the replacement pod redoes.
|
|
1358
1707
|
cursor = !this.shuttingDown && page.has_more ? page.next_cursor : undefined;
|
|
1359
1708
|
} while (cursor);
|
|
1360
|
-
if (processed > 0
|
|
1361
|
-
this.opts.log?.info(`dispatch catch-up: processed ${processed}
|
|
1709
|
+
if (processed > 0) {
|
|
1710
|
+
this.opts.log?.info(`dispatch catch-up: processed ${processed}`);
|
|
1362
1711
|
}
|
|
1363
1712
|
}
|
|
1364
1713
|
async handleHello(data) {
|
|
1365
1714
|
const { client, config, log } = this.opts;
|
|
1366
|
-
this.sessionId = data.session_id ??
|
|
1715
|
+
this.sessionId = data.session_id ?? '';
|
|
1716
|
+
if (this.forkStates.size > 0) {
|
|
1717
|
+
const targetIds = [...this.forkStates.keys()];
|
|
1718
|
+
log?.info(`aborting ${targetIds.length} active fork(s) on reconnect`);
|
|
1719
|
+
for (const targetId of targetIds) {
|
|
1720
|
+
this.abortFork(targetId, 'ws reconnect');
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1367
1723
|
const intervalSec = data.heartbeat_interval > 0 ? data.heartbeat_interval : 30;
|
|
1368
1724
|
try {
|
|
1369
1725
|
const count = await fetchAllChats(client, config.org_id, this.chatInfoMap);
|
|
@@ -1384,7 +1740,7 @@ export class ParallAgentGateway {
|
|
|
1384
1740
|
log?.warn(`heartbeat drift ${drift}ms — event loop may be blocked`);
|
|
1385
1741
|
}
|
|
1386
1742
|
this.lastHeartbeatAt = now;
|
|
1387
|
-
if (this.opts.ws.state !==
|
|
1743
|
+
if (this.opts.ws.state !== 'connected')
|
|
1388
1744
|
return;
|
|
1389
1745
|
this.opts.ws.sendAgentHeartbeat(this.sessionId, {
|
|
1390
1746
|
hostname: os.hostname(),
|
|
@@ -1394,9 +1750,7 @@ export class ParallAgentGateway {
|
|
|
1394
1750
|
uptime: os.uptime(),
|
|
1395
1751
|
});
|
|
1396
1752
|
}, intervalSec * 1000);
|
|
1397
|
-
|
|
1398
|
-
this.hadSuccessfulHello = true;
|
|
1399
|
-
this.catchUpFromDispatch(isFirstHello).catch((err) => {
|
|
1753
|
+
this.catchUpFromDispatch().catch((err) => {
|
|
1400
1754
|
log?.warn(`dispatch catch-up failed: ${String(err)}`);
|
|
1401
1755
|
});
|
|
1402
1756
|
}
|
|
@@ -1445,10 +1799,6 @@ export class ParallAgentGateway {
|
|
|
1445
1799
|
}
|
|
1446
1800
|
if (this.heartbeatTimer)
|
|
1447
1801
|
clearInterval(this.heartbeatTimer);
|
|
1448
|
-
for (const [, dispatch] of this.activeDispatches) {
|
|
1449
|
-
clearInterval(dispatch.typingTimer);
|
|
1450
|
-
}
|
|
1451
|
-
this.activeDispatches.clear();
|
|
1452
1802
|
await this.opts.onBeforeDisconnect?.();
|
|
1453
1803
|
this.opts.ws.disconnect();
|
|
1454
1804
|
this.opts.log?.info(`disconnected`);
|