@parall/agent-core 1.17.0 → 1.18.1
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.d.ts +40 -0
- package/dist/bridge-workspace.d.ts.map +1 -0
- package/dist/bridge-workspace.js +113 -0
- package/dist/event-format.d.ts.map +1 -1
- package/dist/event-format.js +10 -0
- package/dist/gateway-base.d.ts +1 -0
- package/dist/gateway-base.d.ts.map +1 -1
- package/dist/gateway-base.js +87 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/bridge-workspace.ts +107 -0
- package/src/event-format.ts +9 -0
- package/src/gateway-base.ts +88 -2
- package/src/index.ts +1 -0
- package/src/types.ts +2 -2
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared workspace instructions seeded into every self-hosted bridge runtime
|
|
3
|
+
* (Claude Code writes this to `CLAUDE.md`, Codex to `AGENTS.md`). The text and
|
|
4
|
+
* the Parall CLI detection helpers live together so the seeded command
|
|
5
|
+
* examples and the runtime-side suppression logic cannot drift apart. Each
|
|
6
|
+
* bridge package is responsible for its own filesystem write —
|
|
7
|
+
* `@parall/agent-core` stays runtime-neutral and exports only text + pure
|
|
8
|
+
* helpers here.
|
|
9
|
+
*/
|
|
10
|
+
export declare const BRIDGE_WORKSPACE_INSTRUCTIONS = "# Agent workspace\n\nYou are an agent in Parall IM. You participate in chats, handle tasks, and interact exclusively through the Parall CLI.\n\n## Message Model\n\nIncoming events are rendered as structured `[Event: ...]` blocks.\nEach event includes `[Chat: ... (prll://cht_xxx)]` \u2014 use that chat ID (or full URI) when replying.\n\n**Your plain-text output is not delivered to anyone** \u2014 it is recorded as suppressed thinking in your session steps and discarded from the chat.\nTo say something in a chat, you **must** invoke the Parall CLI via your shell/exec tool. To stay silent, simply do not invoke it.\n\n## Parall CLI\n\nAll outbound interactions go through `@parall/cli`. Credentials are pre-injected as environment variables \u2014 no setup needed.\n\n- `npx --yes @parall/cli@latest messages send prll://cht_xxx --text \"...\"` \u2014 reply into the triggering chat\n- `npx --yes @parall/cli@latest dm prll://usr_xxx --text \"...\" [--no-reply]` \u2014 direct message another user\n- `npx --yes @parall/cli@latest tasks update prll://tsk_xxx --status in_progress` \u2014 task state\n- `npx --yes @parall/cli@latest no-reply [--reason \"...\"]` \u2014 explicitly declare this turn silent (audit signal; not required for silence, just clarifies intent)\n\nThe bridge injects Parall context via environment variables. The static credentials `PRLL_API_URL`, `PRLL_API_KEY`, and `PRLL_ORG_ID` are always set. Per-dispatch context \u2014 `PRLL_SESSION_ID`, `PRLL_CHAT_ID`, `PRLL_TRIGGER_MESSAGE_ID`, `PRLL_STEP_ID_FILE` \u2014 is set in subprocess-per-dispatch runtimes (Claude Code); in long-running runtimes (Codex) those may be absent and the CLI will fall back to whatever defaults you supply on the command line.\n\n## Guardrails\n\n- A dispatch may coalesce multiple events. Decide per event whether to reply via `messages send` / `dm` \u2014 events you do not act on simply receive no reply.\n- If an event carries `[Hint: no_reply]`, do not send anything for that event. `no-reply` is optional and only useful as an explicit intent marker.\n- Never try to \"speak\" by typing sentences like \"No response needed\" / \"Noted\" / \"OK\" \u2014 they are discarded, so they accomplish nothing except polluting your session log.\n- Keep CLI replies concise and task-focused.\n\nSee `docs/engineering-design/agent-dm-loop-prevention.md` \u00A7 Layer 0 for why plain text is never auto-projected.\n";
|
|
11
|
+
/** Extracts the `command` string from a shell/bash tool call's input payload. */
|
|
12
|
+
export declare function extractShellCommand(input: unknown): string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the non-flag tokens that follow a Parall CLI invocation in
|
|
15
|
+
* `command`, or `null` if the command isn't a Parall CLI call.
|
|
16
|
+
*
|
|
17
|
+
* Handles all documented launch forms — bare `parall`, `npx [--yes|-y]
|
|
18
|
+
* @parall/cli[@version]`, `pnpm (exec|dlx) parall` — and skips npx flags so
|
|
19
|
+
* `npx --yes` matches the same way as `npx -y`. Uses space-delimited
|
|
20
|
+
* tokenization rather than a regex so that `messages send --no-reply` is
|
|
21
|
+
* correctly classified as a `messages send` invocation (not the receiver-side
|
|
22
|
+
* `no-reply` subcommand).
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseParallCliInvocation(command: string): string[] | null;
|
|
25
|
+
/**
|
|
26
|
+
* Detects whether a shell command invokes the Parall CLI to send a
|
|
27
|
+
* chat-visible side effect (`messages send` or `dm`). Bridge adapters use
|
|
28
|
+
* this to suppress the immediately-following runtime text so the user
|
|
29
|
+
* doesn't see the same message twice — once from the CLI call, once
|
|
30
|
+
* projected from runtime output.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isParallSendCommand(command: string | undefined): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Detects whether a shell command invokes `parall no-reply`, which signals
|
|
35
|
+
* the agent wants the whole turn silenced. Bridge adapters use this to
|
|
36
|
+
* flip a sticky suppression flag for the rest of the dispatch so no text
|
|
37
|
+
* event from that turn is projected into chat.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isParallNoReplyCommand(command: string | undefined): boolean;
|
|
40
|
+
//# sourceMappingURL=bridge-workspace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-workspace.d.ts","sourceRoot":"","sources":["../src/bridge-workspace.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,6BAA6B,m3EA+BzC,CAAC;AAEF,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAItE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAmBzE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAKxE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAI3E"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared workspace instructions seeded into every self-hosted bridge runtime
|
|
3
|
+
* (Claude Code writes this to `CLAUDE.md`, Codex to `AGENTS.md`). The text and
|
|
4
|
+
* the Parall CLI detection helpers live together so the seeded command
|
|
5
|
+
* examples and the runtime-side suppression logic cannot drift apart. Each
|
|
6
|
+
* bridge package is responsible for its own filesystem write —
|
|
7
|
+
* `@parall/agent-core` stays runtime-neutral and exports only text + pure
|
|
8
|
+
* helpers here.
|
|
9
|
+
*/
|
|
10
|
+
export const BRIDGE_WORKSPACE_INSTRUCTIONS = `# Agent workspace
|
|
11
|
+
|
|
12
|
+
You are an agent in Parall IM. You participate in chats, handle tasks, and interact exclusively through the Parall CLI.
|
|
13
|
+
|
|
14
|
+
## Message Model
|
|
15
|
+
|
|
16
|
+
Incoming events are rendered as structured \`[Event: ...]\` blocks.
|
|
17
|
+
Each event includes \`[Chat: ... (prll://cht_xxx)]\` — use that chat ID (or full URI) when replying.
|
|
18
|
+
|
|
19
|
+
**Your plain-text output is not delivered to anyone** — it is recorded as suppressed thinking in your session steps and discarded from the chat.
|
|
20
|
+
To say something in a chat, you **must** invoke the Parall CLI via your shell/exec tool. To stay silent, simply do not invoke it.
|
|
21
|
+
|
|
22
|
+
## Parall CLI
|
|
23
|
+
|
|
24
|
+
All outbound interactions go through \`@parall/cli\`. Credentials are pre-injected as environment variables — no setup needed.
|
|
25
|
+
|
|
26
|
+
- \`npx --yes @parall/cli@latest messages send prll://cht_xxx --text "..."\` — reply into the triggering chat
|
|
27
|
+
- \`npx --yes @parall/cli@latest dm prll://usr_xxx --text "..." [--no-reply]\` — direct message another user
|
|
28
|
+
- \`npx --yes @parall/cli@latest tasks update prll://tsk_xxx --status in_progress\` — task state
|
|
29
|
+
- \`npx --yes @parall/cli@latest no-reply [--reason "..."]\` — explicitly declare this turn silent (audit signal; not required for silence, just clarifies intent)
|
|
30
|
+
|
|
31
|
+
The bridge injects Parall context via environment variables. The static credentials \`PRLL_API_URL\`, \`PRLL_API_KEY\`, and \`PRLL_ORG_ID\` are always set. Per-dispatch context — \`PRLL_SESSION_ID\`, \`PRLL_CHAT_ID\`, \`PRLL_TRIGGER_MESSAGE_ID\`, \`PRLL_STEP_ID_FILE\` — is set in subprocess-per-dispatch runtimes (Claude Code); in long-running runtimes (Codex) those may be absent and the CLI will fall back to whatever defaults you supply on the command line.
|
|
32
|
+
|
|
33
|
+
## Guardrails
|
|
34
|
+
|
|
35
|
+
- A dispatch may coalesce multiple events. Decide per event whether to reply via \`messages send\` / \`dm\` — events you do not act on simply receive no reply.
|
|
36
|
+
- If an event carries \`[Hint: no_reply]\`, do not send anything for that event. \`no-reply\` is optional and only useful as an explicit intent marker.
|
|
37
|
+
- Never try to "speak" by typing sentences like "No response needed" / "Noted" / "OK" — they are discarded, so they accomplish nothing except polluting your session log.
|
|
38
|
+
- Keep CLI replies concise and task-focused.
|
|
39
|
+
|
|
40
|
+
See \`docs/engineering-design/agent-dm-loop-prevention.md\` § Layer 0 for why plain text is never auto-projected.
|
|
41
|
+
`;
|
|
42
|
+
/** Extracts the `command` string from a shell/bash tool call's input payload. */
|
|
43
|
+
export function extractShellCommand(input) {
|
|
44
|
+
if (!input || typeof input !== "object")
|
|
45
|
+
return undefined;
|
|
46
|
+
const command = input.command;
|
|
47
|
+
return typeof command === "string" && command.trim() ? command.trim() : undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns the non-flag tokens that follow a Parall CLI invocation in
|
|
51
|
+
* `command`, or `null` if the command isn't a Parall CLI call.
|
|
52
|
+
*
|
|
53
|
+
* Handles all documented launch forms — bare `parall`, `npx [--yes|-y]
|
|
54
|
+
* @parall/cli[@version]`, `pnpm (exec|dlx) parall` — and skips npx flags so
|
|
55
|
+
* `npx --yes` matches the same way as `npx -y`. Uses space-delimited
|
|
56
|
+
* tokenization rather than a regex so that `messages send --no-reply` is
|
|
57
|
+
* correctly classified as a `messages send` invocation (not the receiver-side
|
|
58
|
+
* `no-reply` subcommand).
|
|
59
|
+
*/
|
|
60
|
+
export function parseParallCliInvocation(command) {
|
|
61
|
+
const tokens = command.replace(/\s+/g, " ").trim().split(" ");
|
|
62
|
+
let i = 0;
|
|
63
|
+
if (tokens[i] === "parall") {
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
else if (tokens[i] === "npx") {
|
|
67
|
+
i++;
|
|
68
|
+
while (i < tokens.length && tokens[i].startsWith("-"))
|
|
69
|
+
i++;
|
|
70
|
+
if (i >= tokens.length || !/^@parall\/cli(?:@.+)?$/.test(tokens[i]))
|
|
71
|
+
return null;
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
else if (tokens[i] === "pnpm") {
|
|
75
|
+
i++;
|
|
76
|
+
if (i < tokens.length && (tokens[i] === "exec" || tokens[i] === "dlx"))
|
|
77
|
+
i++;
|
|
78
|
+
if (i >= tokens.length || tokens[i] !== "parall")
|
|
79
|
+
return null;
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return tokens.slice(i).filter((t) => !t.startsWith("-"));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Detects whether a shell command invokes the Parall CLI to send a
|
|
89
|
+
* chat-visible side effect (`messages send` or `dm`). Bridge adapters use
|
|
90
|
+
* this to suppress the immediately-following runtime text so the user
|
|
91
|
+
* doesn't see the same message twice — once from the CLI call, once
|
|
92
|
+
* projected from runtime output.
|
|
93
|
+
*/
|
|
94
|
+
export function isParallSendCommand(command) {
|
|
95
|
+
if (!command)
|
|
96
|
+
return false;
|
|
97
|
+
const sub = parseParallCliInvocation(command);
|
|
98
|
+
if (!sub || sub.length === 0)
|
|
99
|
+
return false;
|
|
100
|
+
return sub[0] === "dm" || (sub[0] === "messages" && sub[1] === "send");
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Detects whether a shell command invokes `parall no-reply`, which signals
|
|
104
|
+
* the agent wants the whole turn silenced. Bridge adapters use this to
|
|
105
|
+
* flip a sticky suppression flag for the rest of the dispatch so no text
|
|
106
|
+
* event from that turn is projected into chat.
|
|
107
|
+
*/
|
|
108
|
+
export function isParallNoReplyCommand(command) {
|
|
109
|
+
if (!command)
|
|
110
|
+
return false;
|
|
111
|
+
const sub = parseParallCliInvocation(command);
|
|
112
|
+
return sub?.[0] === "no-reply";
|
|
113
|
+
}
|
|
@@ -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,
|
|
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"}
|
package/dist/event-format.js
CHANGED
|
@@ -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
|
package/dist/gateway-base.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/gateway-base.js
CHANGED
|
@@ -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/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./session-state.js";
|
|
|
3
3
|
export * from "./routing.js";
|
|
4
4
|
export * from "./event-format.js";
|
|
5
5
|
export * from "./prompt-fragments.js";
|
|
6
|
+
export * from "./bridge-workspace.js";
|
|
6
7
|
export * from "./dispatch-adapter.js";
|
|
7
8
|
export * from "./gateway-base.js";
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
3
|
+
"version": "1.18.1",
|
|
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.
|
|
25
|
+
"@parall/sdk": "1.18.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.0.0",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared workspace instructions seeded into every self-hosted bridge runtime
|
|
3
|
+
* (Claude Code writes this to `CLAUDE.md`, Codex to `AGENTS.md`). The text and
|
|
4
|
+
* the Parall CLI detection helpers live together so the seeded command
|
|
5
|
+
* examples and the runtime-side suppression logic cannot drift apart. Each
|
|
6
|
+
* bridge package is responsible for its own filesystem write —
|
|
7
|
+
* `@parall/agent-core` stays runtime-neutral and exports only text + pure
|
|
8
|
+
* helpers here.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const BRIDGE_WORKSPACE_INSTRUCTIONS = `# Agent workspace
|
|
12
|
+
|
|
13
|
+
You are an agent in Parall IM. You participate in chats, handle tasks, and interact exclusively through the Parall CLI.
|
|
14
|
+
|
|
15
|
+
## Message Model
|
|
16
|
+
|
|
17
|
+
Incoming events are rendered as structured \`[Event: ...]\` blocks.
|
|
18
|
+
Each event includes \`[Chat: ... (prll://cht_xxx)]\` — use that chat ID (or full URI) when replying.
|
|
19
|
+
|
|
20
|
+
**Your plain-text output is not delivered to anyone** — it is recorded as suppressed thinking in your session steps and discarded from the chat.
|
|
21
|
+
To say something in a chat, you **must** invoke the Parall CLI via your shell/exec tool. To stay silent, simply do not invoke it.
|
|
22
|
+
|
|
23
|
+
## Parall CLI
|
|
24
|
+
|
|
25
|
+
All outbound interactions go through \`@parall/cli\`. Credentials are pre-injected as environment variables — no setup needed.
|
|
26
|
+
|
|
27
|
+
- \`npx --yes @parall/cli@latest messages send prll://cht_xxx --text "..."\` — reply into the triggering chat
|
|
28
|
+
- \`npx --yes @parall/cli@latest dm prll://usr_xxx --text "..." [--no-reply]\` — direct message another user
|
|
29
|
+
- \`npx --yes @parall/cli@latest tasks update prll://tsk_xxx --status in_progress\` — task state
|
|
30
|
+
- \`npx --yes @parall/cli@latest no-reply [--reason "..."]\` — explicitly declare this turn silent (audit signal; not required for silence, just clarifies intent)
|
|
31
|
+
|
|
32
|
+
The bridge injects Parall context via environment variables. The static credentials \`PRLL_API_URL\`, \`PRLL_API_KEY\`, and \`PRLL_ORG_ID\` are always set. Per-dispatch context — \`PRLL_SESSION_ID\`, \`PRLL_CHAT_ID\`, \`PRLL_TRIGGER_MESSAGE_ID\`, \`PRLL_STEP_ID_FILE\` — is set in subprocess-per-dispatch runtimes (Claude Code); in long-running runtimes (Codex) those may be absent and the CLI will fall back to whatever defaults you supply on the command line.
|
|
33
|
+
|
|
34
|
+
## Guardrails
|
|
35
|
+
|
|
36
|
+
- A dispatch may coalesce multiple events. Decide per event whether to reply via \`messages send\` / \`dm\` — events you do not act on simply receive no reply.
|
|
37
|
+
- If an event carries \`[Hint: no_reply]\`, do not send anything for that event. \`no-reply\` is optional and only useful as an explicit intent marker.
|
|
38
|
+
- Never try to "speak" by typing sentences like "No response needed" / "Noted" / "OK" — they are discarded, so they accomplish nothing except polluting your session log.
|
|
39
|
+
- Keep CLI replies concise and task-focused.
|
|
40
|
+
|
|
41
|
+
See \`docs/engineering-design/agent-dm-loop-prevention.md\` § Layer 0 for why plain text is never auto-projected.
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
/** Extracts the `command` string from a shell/bash tool call's input payload. */
|
|
45
|
+
export function extractShellCommand(input: unknown): string | undefined {
|
|
46
|
+
if (!input || typeof input !== "object") return undefined;
|
|
47
|
+
const command = (input as { command?: unknown }).command;
|
|
48
|
+
return typeof command === "string" && command.trim() ? command.trim() : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns the non-flag tokens that follow a Parall CLI invocation in
|
|
53
|
+
* `command`, or `null` if the command isn't a Parall CLI call.
|
|
54
|
+
*
|
|
55
|
+
* Handles all documented launch forms — bare `parall`, `npx [--yes|-y]
|
|
56
|
+
* @parall/cli[@version]`, `pnpm (exec|dlx) parall` — and skips npx flags so
|
|
57
|
+
* `npx --yes` matches the same way as `npx -y`. Uses space-delimited
|
|
58
|
+
* tokenization rather than a regex so that `messages send --no-reply` is
|
|
59
|
+
* correctly classified as a `messages send` invocation (not the receiver-side
|
|
60
|
+
* `no-reply` subcommand).
|
|
61
|
+
*/
|
|
62
|
+
export function parseParallCliInvocation(command: string): string[] | null {
|
|
63
|
+
const tokens = command.replace(/\s+/g, " ").trim().split(" ");
|
|
64
|
+
let i = 0;
|
|
65
|
+
if (tokens[i] === "parall") {
|
|
66
|
+
i++;
|
|
67
|
+
} else if (tokens[i] === "npx") {
|
|
68
|
+
i++;
|
|
69
|
+
while (i < tokens.length && tokens[i].startsWith("-")) i++;
|
|
70
|
+
if (i >= tokens.length || !/^@parall\/cli(?:@.+)?$/.test(tokens[i])) return null;
|
|
71
|
+
i++;
|
|
72
|
+
} else if (tokens[i] === "pnpm") {
|
|
73
|
+
i++;
|
|
74
|
+
if (i < tokens.length && (tokens[i] === "exec" || tokens[i] === "dlx")) i++;
|
|
75
|
+
if (i >= tokens.length || tokens[i] !== "parall") return null;
|
|
76
|
+
i++;
|
|
77
|
+
} else {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return tokens.slice(i).filter((t) => !t.startsWith("-"));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Detects whether a shell command invokes the Parall CLI to send a
|
|
85
|
+
* chat-visible side effect (`messages send` or `dm`). Bridge adapters use
|
|
86
|
+
* this to suppress the immediately-following runtime text so the user
|
|
87
|
+
* doesn't see the same message twice — once from the CLI call, once
|
|
88
|
+
* projected from runtime output.
|
|
89
|
+
*/
|
|
90
|
+
export function isParallSendCommand(command: string | undefined): boolean {
|
|
91
|
+
if (!command) return false;
|
|
92
|
+
const sub = parseParallCliInvocation(command);
|
|
93
|
+
if (!sub || sub.length === 0) return false;
|
|
94
|
+
return sub[0] === "dm" || (sub[0] === "messages" && sub[1] === "send");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Detects whether a shell command invokes `parall no-reply`, which signals
|
|
99
|
+
* the agent wants the whole turn silenced. Bridge adapters use this to
|
|
100
|
+
* flip a sticky suppression flag for the rest of the dispatch so no text
|
|
101
|
+
* event from that turn is projected into chat.
|
|
102
|
+
*/
|
|
103
|
+
export function isParallNoReplyCommand(command: string | undefined): boolean {
|
|
104
|
+
if (!command) return false;
|
|
105
|
+
const sub = parseParallCliInvocation(command);
|
|
106
|
+
return sub?.[0] === "no-reply";
|
|
107
|
+
}
|
package/src/event-format.ts
CHANGED
|
@@ -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
|
package/src/gateway-base.ts
CHANGED
|
@@ -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/index.ts
CHANGED
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
|
};
|