@parall/agent-core 1.17.0 → 1.18.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.
@@ -1 +1 @@
1
- {"version":3,"file":"event-format.d.ts","sourceRoot":"","sources":["../src/event-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CA+BzD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CASnE"}
1
+ {"version":3,"file":"event-format.d.ts","sourceRoot":"","sources":["../src/event-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAwCzD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CASnE"}
@@ -25,6 +25,16 @@ export function buildEventBody(event) {
25
25
  }
26
26
  lines.push("", event.body);
27
27
  }
28
+ else if (event.type === "task_comment") {
29
+ lines.push(`[Event: task.comment.created]`);
30
+ const taskLabel = event.targetName
31
+ ? `${event.targetName} (prll://${event.targetId})`
32
+ : `prll://${event.targetId}`;
33
+ lines.push(`[Task: ${taskLabel}]`);
34
+ lines.push(`[From: ${event.senderName} (prll://${event.senderId})]`);
35
+ lines.push(`[Comment ID: prll://${event.messageId}]`);
36
+ lines.push("", event.body);
37
+ }
28
38
  else {
29
39
  lines.push(`[Event: task.assigned]`);
30
40
  const taskLabel = event.targetName
@@ -62,6 +62,7 @@ export declare class ParallAgentGateway {
62
62
  private buildMessageDispatchDecision;
63
63
  private handleMessage;
64
64
  private handleTaskAssignment;
65
+ private handleTaskComment;
65
66
  private catchUpFromDispatch;
66
67
  private handleHello;
67
68
  private shutdown;
@@ -1 +1 @@
1
- {"version":3,"file":"gateway-base.d.ts","sourceRoot":"","sources":["../src/gateway-base.ts"],"names":[],"mappings":"AAIA,OAAO,EAAuB,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,KAAK,EACV,qBAAqB,EAQtB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAEV,eAAe,EAGf,aAAa,EAEd,MAAM,uBAAuB,CAAC;AAoD/B,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,YAAY,CAAC;IACrB,EAAE,EAAE,QAAQ,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE;QACN,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,eAAe,EAAE,eAAe,CAAC;IACjC,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wBAAwB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACtE,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,QAAQ,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACjH,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACjD,CAAC;AAkCF,qBAAa,kBAAkB;IAuBjB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAtBjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqC;IACtE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsC;IACjE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAK5B;IAEF,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAQ;IAChD,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;gBAEjB,IAAI,EAAE,oBAAoB;IAIjD,GAAG,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IA8ElD,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,oBAAoB;YAmBd,eAAe;YAqBf,iBAAiB;IAsF/B,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,eAAe;YAQT,gCAAgC;YAMhC,WAAW;YAyDX,gBAAgB;YAmEhB,eAAe;YAmDf,kBAAkB;YAoElB,kBAAkB;YAkBlB,4BAA4B;YAoD5B,aAAa;YA8Bb,oBAAoB;YAmCpB,mBAAmB;YAoGnB,WAAW;YA0DX,QAAQ;CAsBvB"}
1
+ {"version":3,"file":"gateway-base.d.ts","sourceRoot":"","sources":["../src/gateway-base.ts"],"names":[],"mappings":"AAIA,OAAO,EAAuB,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,KAAK,EACV,qBAAqB,EAUtB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAEV,eAAe,EAGf,aAAa,EAEd,MAAM,uBAAuB,CAAC;AAoD/B,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,YAAY,CAAC;IACrB,EAAE,EAAE,QAAQ,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE;QACN,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,eAAe,EAAE,eAAe,CAAC;IACjC,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wBAAwB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACtE,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,QAAQ,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACjH,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACjD,CAAC;AAkCF,qBAAa,kBAAkB;IAuBjB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAtBjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqC;IACtE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsC;IACjE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAK5B;IAEF,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAQ;IAChD,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;gBAEjB,IAAI,EAAE,oBAAoB;IAIjD,GAAG,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IA2FlD,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,oBAAoB;YAmBd,eAAe;YAqBf,iBAAiB;IAsF/B,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,eAAe;YAQT,gCAAgC;YAMhC,WAAW;YAyDX,gBAAgB;YAmEhB,eAAe;YAmDf,kBAAkB;YAoElB,kBAAkB;YAkBlB,4BAA4B;YAoD5B,aAAa;YA8Bb,oBAAoB;YAmCpB,iBAAiB;YAqEjB,mBAAmB;YAsGnB,WAAW;YA0DX,QAAQ;CAsBvB"}
@@ -121,6 +121,21 @@ export class ParallAgentGateway {
121
121
  this.opts.log?.error(`parall[${this.opts.accountId}]: task dispatch failed for ${data.id}: ${String(err)}`);
122
122
  }
123
123
  });
124
+ ws.on("dispatch.new", async (data) => {
125
+ if (data.event_type !== "task_comment")
126
+ return;
127
+ if (!data.source_id || !data.task_id)
128
+ return;
129
+ try {
130
+ const dispatched = await this.handleTaskComment(data.source_id, data.task_id, data.actor_id);
131
+ if (dispatched) {
132
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => { });
133
+ }
134
+ }
135
+ catch (err) {
136
+ this.opts.log?.error(`parall[${this.opts.accountId}]: task comment dispatch failed for ${data.source_id}: ${String(err)}`);
137
+ }
138
+ });
124
139
  this.opts.log?.info(`parall[${this.opts.accountId}]: connecting to ${this.opts.connectionLabel ?? "Parall WS"}...`);
125
140
  await ws.connect();
126
141
  return new Promise((resolve) => {
@@ -198,8 +213,8 @@ export class ParallAgentGateway {
198
213
  target_type: target.target_type,
199
214
  target_id: target.target_id,
200
215
  content: {
201
- trigger_type: event.type === "task" ? "task_assign" : "mention",
202
- trigger_ref: event.type === "task" ? { task_id: event.targetId } : { message_id: event.messageId },
216
+ trigger_type: event.type === "task" ? "task_assign" : event.type === "task_comment" ? "task_comment" : "mention",
217
+ trigger_ref: event.type === "task" ? { task_id: event.targetId } : event.type === "task_comment" ? { comment_id: event.messageId, task_id: event.targetId } : { message_id: event.messageId },
203
218
  sender_id: event.senderId,
204
219
  sender_name: event.senderName,
205
220
  summary: event.body.substring(0, 200),
@@ -673,6 +688,73 @@ export class ParallAgentGateway {
673
688
  }
674
689
  return dispatched;
675
690
  }
691
+ async handleTaskComment(commentId, taskId, actorId) {
692
+ const dedupeKey = `comment:${commentId}`;
693
+ if (this.dispatchedTasks.has(dedupeKey))
694
+ return false;
695
+ this.dispatchedTasks.add(dedupeKey);
696
+ let comment = null;
697
+ try {
698
+ comment = await this.opts.client.getComment(this.opts.config.org_id, commentId);
699
+ }
700
+ catch (err) {
701
+ const status = err?.status;
702
+ if (status === 404) {
703
+ // Comment deleted — ack stale dispatch so it doesn't pile up.
704
+ this.opts.log?.info(`parall[${this.opts.accountId}]: skipping deleted comment ${commentId}, acking stale dispatch`);
705
+ this.dispatchedTasks.delete(dedupeKey);
706
+ return true; // caller will ack
707
+ }
708
+ this.dispatchedTasks.delete(dedupeKey);
709
+ return false; // transient error — leave pending for retry
710
+ }
711
+ if (!comment) {
712
+ this.dispatchedTasks.delete(dedupeKey);
713
+ return true; // null response = gone, ack stale dispatch
714
+ }
715
+ let task = null;
716
+ try {
717
+ task = await this.opts.client.getTask(this.opts.config.org_id, taskId);
718
+ }
719
+ catch { /* task context is optional */ }
720
+ const taskLabel = task ? `${task.identifier ?? task.id} "${task.title}"` : taskId;
721
+ this.opts.log?.info(`parall[${this.opts.accountId}]: task comment on ${taskLabel} by ${actorId ?? "unknown"}`);
722
+ const parts = [];
723
+ if (task) {
724
+ parts.push(`Task: ${task.title} (prll://${task.id})`);
725
+ parts.push(`Status: ${task.status}`, `Priority: ${task.priority}`);
726
+ }
727
+ else {
728
+ parts.push(`Task: prll://${taskId}`);
729
+ }
730
+ parts.push(`Comment by: ${comment.author?.display_name ?? actorId ?? "unknown"} (prll://${comment.author_id})`);
731
+ parts.push("", comment.body);
732
+ const event = {
733
+ type: "task_comment",
734
+ targetId: taskId,
735
+ targetName: task?.identifier ?? undefined,
736
+ targetType: "task",
737
+ senderId: comment.author_id,
738
+ senderName: comment.author?.display_name ?? actorId ?? "unknown",
739
+ messageId: commentId,
740
+ body: parts.join("\n"),
741
+ ackSourceType: "comment",
742
+ ackSourceId: commentId,
743
+ };
744
+ let dispatched;
745
+ try {
746
+ dispatched = await this.handleInboundEvent(event);
747
+ }
748
+ catch (err) {
749
+ // Clear dedupe key so the event remains retryable on next catch-up.
750
+ this.dispatchedTasks.delete(dedupeKey);
751
+ throw err;
752
+ }
753
+ if (!dispatched) {
754
+ this.dispatchedTasks.delete(dedupeKey);
755
+ }
756
+ return dispatched;
757
+ }
676
758
  async catchUpFromDispatch(coldStart = false) {
677
759
  const minAge = coldStart ? Date.now() - this.COLD_START_WINDOW_MS : 0;
678
760
  let cursor;
@@ -722,6 +804,9 @@ export class ParallAgentGateway {
722
804
  dispatched = true;
723
805
  }
724
806
  }
807
+ else if (item.event_type === "task_comment" && item.source_id && item.task_id) {
808
+ dispatched = await this.handleTaskComment(item.source_id, item.task_id, item.actor_id);
809
+ }
725
810
  else if (item.event_type === "message" && item.source_id && item.chat_id) {
726
811
  if (!this.tryClaimMessage(item.source_id))
727
812
  continue;
package/dist/types.d.ts CHANGED
@@ -18,7 +18,7 @@ export type DispatchState = {
18
18
  };
19
19
  /** Normalized inbound event from Parall. */
20
20
  export type ParallEvent = {
21
- type: "message" | "task";
21
+ type: "message" | "task" | "task_comment";
22
22
  targetId: string;
23
23
  targetName?: string;
24
24
  targetType?: string;
@@ -34,7 +34,7 @@ export type ParallEvent = {
34
34
  fileSize: number;
35
35
  mimeType: string;
36
36
  }>;
37
- ackSourceType?: "message" | "task_activity";
37
+ ackSourceType?: "message" | "task_activity" | "comment";
38
38
  ackSourceId?: string;
39
39
  };
40
40
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,MAAM,UAAU,GAAG;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,kBAAkB,EAAE,UAAU,EAAE,CAAC;IACjC,UAAU,EAAE,WAAW,EAAE,CAAC;CAC3B,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,aAAa,CAAC,EAAE,SAAS,GAAG,eAAe,CAAC;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,MAAM,MAAM,UAAU,GAAG;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,kBAAkB,EAAE,UAAU,EAAE,CAAC;IACjC,UAAU,EAAE,WAAW,EAAE,CAAC;CAC3B,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,cAAc,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,aAAa,CAAC,EAAE,SAAS,GAAG,eAAe,GAAG,SAAS,CAAC;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/agent-core",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "Shared agent runtime orchestration helpers for Parall",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,7 +22,7 @@
22
22
  "src"
23
23
  ],
24
24
  "dependencies": {
25
- "@parall/sdk": "1.17.0"
25
+ "@parall/sdk": "1.18.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.0.0",
@@ -25,6 +25,15 @@ export function buildEventBody(event: ParallEvent): string {
25
25
  }
26
26
  }
27
27
  lines.push("", event.body);
28
+ } else if (event.type === "task_comment") {
29
+ lines.push(`[Event: task.comment.created]`);
30
+ const taskLabel = event.targetName
31
+ ? `${event.targetName} (prll://${event.targetId})`
32
+ : `prll://${event.targetId}`;
33
+ lines.push(`[Task: ${taskLabel}]`);
34
+ lines.push(`[From: ${event.senderName} (prll://${event.senderId})]`);
35
+ lines.push(`[Comment ID: prll://${event.messageId}]`);
36
+ lines.push("", event.body);
28
37
  } else {
29
38
  lines.push(`[Event: task.assigned]`);
30
39
  const taskLabel = event.targetName
@@ -7,6 +7,8 @@ import type {
7
7
  AgentConfigUpdateData,
8
8
  Chat,
9
9
  ChatUpdateData,
10
+ Comment,
11
+ DispatchNewData,
10
12
  HelloData,
11
13
  MessageNewData,
12
14
  Task,
@@ -222,6 +224,19 @@ export class ParallAgentGateway {
222
224
  }
223
225
  });
224
226
 
227
+ ws.on("dispatch.new", async (data: DispatchNewData) => {
228
+ if (data.event_type !== "task_comment") return;
229
+ if (!data.source_id || !data.task_id) return;
230
+ try {
231
+ const dispatched = await this.handleTaskComment(data.source_id, data.task_id, data.actor_id);
232
+ if (dispatched) {
233
+ this.opts.client.ackDispatchByID(this.opts.config.org_id, data.id).catch(() => {});
234
+ }
235
+ } catch (err) {
236
+ this.opts.log?.error(`parall[${this.opts.accountId}]: task comment dispatch failed for ${data.source_id}: ${String(err)}`);
237
+ }
238
+ });
239
+
225
240
  this.opts.log?.info(`parall[${this.opts.accountId}]: connecting to ${this.opts.connectionLabel ?? "Parall WS"}...`);
226
241
  await ws.connect();
227
242
 
@@ -299,8 +314,8 @@ export class ParallAgentGateway {
299
314
  target_type: target.target_type,
300
315
  target_id: target.target_id,
301
316
  content: {
302
- trigger_type: event.type === "task" ? "task_assign" : "mention",
303
- trigger_ref: event.type === "task" ? { task_id: event.targetId } : { message_id: event.messageId },
317
+ trigger_type: event.type === "task" ? "task_assign" : event.type === "task_comment" ? "task_comment" : "mention",
318
+ trigger_ref: event.type === "task" ? { task_id: event.targetId } : event.type === "task_comment" ? { comment_id: event.messageId, task_id: event.targetId } : { message_id: event.messageId },
304
319
  sender_id: event.senderId,
305
320
  sender_name: event.senderName,
306
321
  summary: event.body.substring(0, 200),
@@ -798,6 +813,75 @@ export class ParallAgentGateway {
798
813
  return dispatched;
799
814
  }
800
815
 
816
+ private async handleTaskComment(commentId: string, taskId: string, actorId: string | null): Promise<boolean> {
817
+ const dedupeKey = `comment:${commentId}`;
818
+ if (this.dispatchedTasks.has(dedupeKey)) return false;
819
+ this.dispatchedTasks.add(dedupeKey);
820
+
821
+ let comment: Comment | null = null;
822
+ try {
823
+ comment = await this.opts.client.getComment(this.opts.config.org_id, commentId);
824
+ } catch (err: unknown) {
825
+ const status = (err as { status?: number })?.status;
826
+ if (status === 404) {
827
+ // Comment deleted — ack stale dispatch so it doesn't pile up.
828
+ this.opts.log?.info(`parall[${this.opts.accountId}]: skipping deleted comment ${commentId}, acking stale dispatch`);
829
+ this.dispatchedTasks.delete(dedupeKey);
830
+ return true; // caller will ack
831
+ }
832
+ this.dispatchedTasks.delete(dedupeKey);
833
+ return false; // transient error — leave pending for retry
834
+ }
835
+ if (!comment) {
836
+ this.dispatchedTasks.delete(dedupeKey);
837
+ return true; // null response = gone, ack stale dispatch
838
+ }
839
+
840
+ let task: Task | null = null;
841
+ try {
842
+ task = await this.opts.client.getTask(this.opts.config.org_id, taskId);
843
+ } catch { /* task context is optional */ }
844
+
845
+ const taskLabel = task ? `${task.identifier ?? task.id} "${task.title}"` : taskId;
846
+ this.opts.log?.info(`parall[${this.opts.accountId}]: task comment on ${taskLabel} by ${actorId ?? "unknown"}`);
847
+
848
+ const parts: string[] = [];
849
+ if (task) {
850
+ parts.push(`Task: ${task.title} (prll://${task.id})`);
851
+ parts.push(`Status: ${task.status}`, `Priority: ${task.priority}`);
852
+ } else {
853
+ parts.push(`Task: prll://${taskId}`);
854
+ }
855
+ parts.push(`Comment by: ${comment.author?.display_name ?? actorId ?? "unknown"} (prll://${comment.author_id})`);
856
+ parts.push("", comment.body);
857
+
858
+ const event: ParallEvent = {
859
+ type: "task_comment",
860
+ targetId: taskId,
861
+ targetName: task?.identifier ?? undefined,
862
+ targetType: "task",
863
+ senderId: comment.author_id,
864
+ senderName: comment.author?.display_name ?? actorId ?? "unknown",
865
+ messageId: commentId,
866
+ body: parts.join("\n"),
867
+ ackSourceType: "comment",
868
+ ackSourceId: commentId,
869
+ };
870
+
871
+ let dispatched: boolean;
872
+ try {
873
+ dispatched = await this.handleInboundEvent(event);
874
+ } catch (err) {
875
+ // Clear dedupe key so the event remains retryable on next catch-up.
876
+ this.dispatchedTasks.delete(dedupeKey);
877
+ throw err;
878
+ }
879
+ if (!dispatched) {
880
+ this.dispatchedTasks.delete(dedupeKey);
881
+ }
882
+ return dispatched;
883
+ }
884
+
801
885
  private async catchUpFromDispatch(coldStart = false) {
802
886
  const minAge = coldStart ? Date.now() - this.COLD_START_WINDOW_MS : 0;
803
887
  let cursor: string | undefined;
@@ -845,6 +929,8 @@ export class ParallAgentGateway {
845
929
  } else {
846
930
  dispatched = true;
847
931
  }
932
+ } else if (item.event_type === "task_comment" && item.source_id && item.task_id) {
933
+ dispatched = await this.handleTaskComment(item.source_id, item.task_id, item.actor_id);
848
934
  } else if (item.event_type === "message" && item.source_id && item.chat_id) {
849
935
  if (!this.tryClaimMessage(item.source_id)) continue;
850
936
  let msg: Awaited<ReturnType<typeof this.opts.client.getMessage>> | null = null;
package/src/types.ts CHANGED
@@ -16,7 +16,7 @@ export type DispatchState = {
16
16
 
17
17
  /** Normalized inbound event from Parall. */
18
18
  export type ParallEvent = {
19
- type: "message" | "task";
19
+ type: "message" | "task" | "task_comment";
20
20
  targetId: string;
21
21
  targetName?: string;
22
22
  targetType?: string;
@@ -32,6 +32,6 @@ export type ParallEvent = {
32
32
  fileSize: number;
33
33
  mimeType: string;
34
34
  }>;
35
- ackSourceType?: "message" | "task_activity";
35
+ ackSourceType?: "message" | "task_activity" | "comment";
36
36
  ackSourceId?: string;
37
37
  };