@parall/agent-core 1.16.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;AAmD/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.16.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.16.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,
@@ -65,6 +67,7 @@ type DispatchableMessage = {
65
67
  content: unknown;
66
68
  thread_root_id?: string | null;
67
69
  hints?: { no_reply?: boolean } | null;
70
+ attachments?: Array<{ id: string; file_name: string; file_size: number; mime_type: string }>;
68
71
  };
69
72
 
70
73
  type MessageDispatchDecision =
@@ -221,6 +224,19 @@ export class ParallAgentGateway {
221
224
  }
222
225
  });
223
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
+
224
240
  this.opts.log?.info(`parall[${this.opts.accountId}]: connecting to ${this.opts.connectionLabel ?? "Parall WS"}...`);
225
241
  await ws.connect();
226
242
 
@@ -298,8 +314,8 @@ export class ParallAgentGateway {
298
314
  target_type: target.target_type,
299
315
  target_id: target.target_id,
300
316
  content: {
301
- trigger_type: event.type === "task" ? "task_assign" : "mention",
302
- 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 },
303
319
  sender_id: event.senderId,
304
320
  sender_name: event.senderName,
305
321
  summary: event.body.substring(0, 200),
@@ -693,7 +709,7 @@ export class ParallAgentGateway {
693
709
 
694
710
  const content = message.content as TextContent;
695
711
  const body = content.text?.trim() ?? "";
696
- const hasAttachments = (message as { attachments?: unknown[] }).attachments?.length;
712
+ const hasAttachments = message.attachments?.length;
697
713
 
698
714
  if (!body && !hasAttachments) return { action: "skip" };
699
715
 
@@ -705,7 +721,7 @@ export class ParallAgentGateway {
705
721
  if (!isMentioned) return { action: "skip" };
706
722
  }
707
723
 
708
- const attachments = ((message as { attachments?: Array<{ id: string; file_name: string; file_size: number; mime_type: string }> }).attachments ?? []).map((a) => ({
724
+ const attachments = (message.attachments ?? []).map((a) => ({
709
725
  id: a.id,
710
726
  fileName: a.file_name,
711
727
  fileSize: a.file_size,
@@ -797,6 +813,75 @@ export class ParallAgentGateway {
797
813
  return dispatched;
798
814
  }
799
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
+
800
885
  private async catchUpFromDispatch(coldStart = false) {
801
886
  const minAge = coldStart ? Date.now() - this.COLD_START_WINDOW_MS : 0;
802
887
  let cursor: string | undefined;
@@ -844,6 +929,8 @@ export class ParallAgentGateway {
844
929
  } else {
845
930
  dispatched = true;
846
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);
847
934
  } else if (item.event_type === "message" && item.source_id && item.chat_id) {
848
935
  if (!this.tryClaimMessage(item.source_id)) continue;
849
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
  };