paperclip-plugin-mattermost 0.18.0 → 0.18.2
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/README.md +1 -1
- package/dist/manifest.js +1 -1
- package/dist/worker.js +84 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ PAPERCLIP_REF=<commit> node build.mjs # build for another Paperclip version
|
|
|
85
85
|
Bump `version` in `package.json` and `src/manifest.ts`, commit, then tag and push:
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
git tag -a v0.18.
|
|
88
|
+
git tag -a v0.18.2 -m v0.18.2 && git push origin main v0.18.2
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
GitHub Actions builds, tests and publishes to npm through npm trusted publishing, so no npm token is involved.
|
package/dist/manifest.js
CHANGED
|
@@ -84,7 +84,7 @@ var route = (routeKey, method, path) => ({ routeKey, method, path, auth: "agent"
|
|
|
84
84
|
var manifest = {
|
|
85
85
|
id: "mattermost-bridge",
|
|
86
86
|
apiVersion: 1,
|
|
87
|
-
version: "0.18.
|
|
87
|
+
version: "0.18.2",
|
|
88
88
|
displayName: "Mattermost",
|
|
89
89
|
description: "Every agent becomes its own Mattermost bot. DM an agent or mention it in a channel and it answers in the thread, showing its steps as it works; follow-ups continue the conversation. Paperclip approvals arrive with Approve / Reject buttons.",
|
|
90
90
|
author: "Haseeb Azhar",
|
package/dist/worker.js
CHANGED
|
@@ -45383,11 +45383,16 @@ var WorkLinks = class {
|
|
|
45383
45383
|
const c = (await this.d.comments(issueId)).find((x) => x.id === commentId);
|
|
45384
45384
|
if (!c?.body?.trim()) return;
|
|
45385
45385
|
const body = c.body.trim().slice(0, 12e3);
|
|
45386
|
-
const
|
|
45387
|
-
const text2 = own2 ? body : `**${c.authorAgentId ? this.d.agentName(c.authorAgentId) : "A team member"}** commented on ${this.label(hit.link)}:
|
|
45386
|
+
const quoted = `**${c.authorAgentId ? this.d.agentName(c.authorAgentId) : "A team member"}** commented on ${this.label(hit.link)}:
|
|
45388
45387
|
|
|
45389
45388
|
${body}`;
|
|
45390
|
-
|
|
45389
|
+
if (c.authorAgentId && c.authorAgentId !== hit.link.agentId && this.d.hasBot(c.authorAgentId)) {
|
|
45390
|
+
try {
|
|
45391
|
+
return await this.d.post(c.authorAgentId, hit.channelId, "", body);
|
|
45392
|
+
} catch {
|
|
45393
|
+
}
|
|
45394
|
+
}
|
|
45395
|
+
await this.d.post(hit.link.agentId, hit.channelId, "", c.authorAgentId === hit.link.agentId ? body : quoted);
|
|
45391
45396
|
}
|
|
45392
45397
|
/**
|
|
45393
45398
|
* A task asked for in a Mattermost thread (created by the agent while answering there): when it is
|
|
@@ -45399,14 +45404,20 @@ ${body}`;
|
|
|
45399
45404
|
if (!at) return;
|
|
45400
45405
|
const task2 = await this.d.task(issueId);
|
|
45401
45406
|
if (!task2 || task2.status === at.status || !THREAD_REPORTS[task2.status]) return;
|
|
45407
|
+
const by = [at.agentId, task2.assigneeAgentId].find((a) => Boolean(a && this.d.hasBot(a)));
|
|
45408
|
+
if (!by) return;
|
|
45402
45409
|
await this.d.store.set(`issuethread:${issueId}`, { ...at, status: task2.status });
|
|
45403
45410
|
const last = (await this.d.comments(issueId)).at(-1)?.body?.trim();
|
|
45404
45411
|
const outcome = last ? `
|
|
45405
45412
|
|
|
45406
45413
|
${last.length > 3e3 ? last.slice(0, 3e3) + "\u2026" : last}` : "";
|
|
45407
|
-
const
|
|
45408
|
-
|
|
45409
|
-
|
|
45414
|
+
const doer = task2.assigneeAgentId && task2.assigneeAgentId !== by ? ` _(by ${this.d.agentName(task2.assigneeAgentId)})_` : "";
|
|
45415
|
+
try {
|
|
45416
|
+
await this.d.post(by, at.channelId, at.rootId, `${THREAD_REPORTS[task2.status]} **${task2.identifier ?? "The task"}** ${task2.title}${doer}${outcome}`);
|
|
45417
|
+
} catch (err) {
|
|
45418
|
+
await this.d.store.set(`issuethread:${issueId}`, at);
|
|
45419
|
+
throw err;
|
|
45420
|
+
}
|
|
45410
45421
|
}
|
|
45411
45422
|
/** The task changed: note a new status; on done or cancelled, archive and report back. */
|
|
45412
45423
|
async updated(issueId) {
|
|
@@ -45530,6 +45541,9 @@ ${HELP}`;
|
|
|
45530
45541
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
45531
45542
|
|
|
45532
45543
|
// src/bridge.ts
|
|
45544
|
+
var IN_FLIGHT = "inflight";
|
|
45545
|
+
var IN_FLIGHT_MAX_AGE = 3 * 60 * 60 * 1e3;
|
|
45546
|
+
var RESUME_PROMPT = "The Mattermost plugin restarted while you were working on the conversation above, so your reply did not reach the thread. When you have finished, give your answer to the latest message again; it is posted in the thread.";
|
|
45533
45547
|
var DEFAULT_TURN_LIMIT = 10;
|
|
45534
45548
|
var TYPING_EVERY_MS = 3e3;
|
|
45535
45549
|
var OLD_PLACEHOLDER = /^_(thinking…|.+ is working on it…)_/;
|
|
@@ -45790,25 +45804,82 @@ To send files back with your reply, save them in ${outbox}` : "");
|
|
|
45790
45804
|
this.active.set(member.agentId, here);
|
|
45791
45805
|
try {
|
|
45792
45806
|
let sessionId = thread && thread.agentId === member.agentId ? thread.sessionId : null;
|
|
45807
|
+
const flight = (id) => this.inFlight(rootId, { agentId: member.agentId, sessionId: id, channelId: post.channel_id, channelType: evt.channelType, channelName: evt.channelName, at: Date.now() });
|
|
45793
45808
|
try {
|
|
45794
45809
|
if (!sessionId) throw new Error("no session yet");
|
|
45810
|
+
await flight(sessionId);
|
|
45795
45811
|
reply = await this.agents.ask(sessionId, prompt);
|
|
45796
|
-
} catch (
|
|
45797
|
-
if (sessionId) this.log("session unusable, starting a new one", { sessionId, err: String(
|
|
45812
|
+
} catch (first) {
|
|
45813
|
+
if (sessionId) this.log("session unusable, starting a new one", { sessionId, err: String(first) });
|
|
45798
45814
|
sessionId = await this.agents.createSession(member.agentId, `mattermost:${rootId}`);
|
|
45799
45815
|
await this.store.set(threadKey, { agentId: member.agentId, sessionId });
|
|
45816
|
+
await flight(sessionId);
|
|
45800
45817
|
reply = await this.agents.ask(sessionId, prompt);
|
|
45801
45818
|
}
|
|
45802
45819
|
} catch (err) {
|
|
45803
45820
|
if (this.active.get(member.agentId) === here) this.active.delete(member.agentId);
|
|
45804
45821
|
this.inbox.delete(member.agentId);
|
|
45822
|
+
await this.inFlight(rootId, null);
|
|
45805
45823
|
await member.chat.createPost(post.channel_id, rootId, `:warning: ${member.agentName} could not answer: ${String(err?.message ?? err).slice(0, 500)}`);
|
|
45806
45824
|
return;
|
|
45807
45825
|
}
|
|
45808
45826
|
if (this.active.get(member.agentId) === here) this.active.delete(member.agentId);
|
|
45809
45827
|
this.inbox.delete(member.agentId);
|
|
45828
|
+
await this.inFlight(rootId, null);
|
|
45829
|
+
await this.postReply(member, { channelId: post.channel_id, rootId, channelType: evt.channelType, channelName: evt.channelName }, reply, outbox);
|
|
45830
|
+
}
|
|
45831
|
+
/** Records (or with null clears) the reply this thread is waiting for. */
|
|
45832
|
+
async inFlight(rootId, f) {
|
|
45833
|
+
const all = await this.store.get(IN_FLIGHT) ?? {};
|
|
45834
|
+
if (f) all[rootId] = f;
|
|
45835
|
+
else delete all[rootId];
|
|
45836
|
+
await this.store.set(IN_FLIGHT, all);
|
|
45837
|
+
}
|
|
45838
|
+
/**
|
|
45839
|
+
* After a plugin restart: every reply that was being waited for is asked for again from the same
|
|
45840
|
+
* agent session (which queues behind the run still going), and posted in its thread when it comes.
|
|
45841
|
+
*/
|
|
45842
|
+
async resume() {
|
|
45843
|
+
const all = await this.store.get(IN_FLIGHT) ?? {};
|
|
45844
|
+
let n = 0;
|
|
45845
|
+
for (const [rootId, f] of Object.entries(all)) {
|
|
45846
|
+
const member = this.roster().byAgentId(f.agentId);
|
|
45847
|
+
if (!member || Date.now() - f.at > IN_FLIGHT_MAX_AGE) {
|
|
45848
|
+
await this.inFlight(rootId, null);
|
|
45849
|
+
continue;
|
|
45850
|
+
}
|
|
45851
|
+
n++;
|
|
45852
|
+
const prev = this.queues.get(rootId) ?? Promise.resolve();
|
|
45853
|
+
const next = prev.then(async () => {
|
|
45854
|
+
const typing = () => {
|
|
45855
|
+
void member.chat.typing(f.channelId, rootId).catch(() => {
|
|
45856
|
+
});
|
|
45857
|
+
};
|
|
45858
|
+
typing();
|
|
45859
|
+
const timer = setInterval(typing, TYPING_EVERY_MS);
|
|
45860
|
+
try {
|
|
45861
|
+
const reply = await this.agents.ask(f.sessionId, RESUME_PROMPT);
|
|
45862
|
+
await this.inFlight(rootId, null);
|
|
45863
|
+
await this.postReply(member, { channelId: f.channelId, rootId, channelType: f.channelType, channelName: f.channelName }, reply, null);
|
|
45864
|
+
} catch (err) {
|
|
45865
|
+
await this.inFlight(rootId, null);
|
|
45866
|
+
await member.chat.createPost(f.channelId, rootId, `:warning: ${member.agentName}'s reply was lost when the plugin restarted: ${String(err?.message ?? err).slice(0, 300)}. Ask again here.`).catch(() => {
|
|
45867
|
+
});
|
|
45868
|
+
} finally {
|
|
45869
|
+
clearInterval(timer);
|
|
45870
|
+
}
|
|
45871
|
+
}).catch((err) => this.log("resume failed", { err: String(err) }));
|
|
45872
|
+
this.queues.set(rootId, next);
|
|
45873
|
+
void next.finally(() => {
|
|
45874
|
+
if (this.queues.get(rootId) === next) this.queues.delete(rootId);
|
|
45875
|
+
});
|
|
45876
|
+
}
|
|
45877
|
+
return n;
|
|
45878
|
+
}
|
|
45879
|
+
async postReply(member, origin, reply, outbox) {
|
|
45880
|
+
const post = { channel_id: origin.channelId };
|
|
45881
|
+
const rootId = origin.rootId;
|
|
45810
45882
|
const { text: body, choices } = extractChoices(linkTasks(absoluteLinks(reply ?? "", this.linkBase), this.linkBase, this.taskPrefix));
|
|
45811
|
-
const origin = { channelId: post.channel_id, rootId, channelType: evt.channelType, channelName: evt.channelName };
|
|
45812
45883
|
const buttons = choices.length && this.choiceButtons ? this.choiceButtons(origin, choices) : null;
|
|
45813
45884
|
const shown = choices.length && !buttons ? `${body}
|
|
45814
45885
|
|
|
@@ -46305,7 +46376,7 @@ var route = (routeKey, method, path3) => ({ routeKey, method, path: path3, auth:
|
|
|
46305
46376
|
var manifest = {
|
|
46306
46377
|
id: "mattermost-bridge",
|
|
46307
46378
|
apiVersion: 1,
|
|
46308
|
-
version: "0.18.
|
|
46379
|
+
version: "0.18.2",
|
|
46309
46380
|
displayName: "Mattermost",
|
|
46310
46381
|
description: "Every agent becomes its own Mattermost bot. DM an agent or mention it in a channel and it answers in the thread, showing its steps as it works; follow-ups continue the conversation. Paperclip approvals arrive with Approve / Reject buttons.",
|
|
46311
46382
|
author: "Haseeb Azhar",
|
|
@@ -46681,7 +46752,9 @@ var Tenant = class {
|
|
|
46681
46752
|
this.status = `agent bot sync failed: ${errText(err)}`;
|
|
46682
46753
|
ctx.logger.error(this.status, { companyId });
|
|
46683
46754
|
});
|
|
46684
|
-
void sync()
|
|
46755
|
+
void sync().then(() => bridge.resume()).then((n) => {
|
|
46756
|
+
if (n) ctx.logger.info("resuming replies interrupted by a restart", { companyId, threads: n });
|
|
46757
|
+
}).catch((err) => warn("could not resume interrupted replies", { err: errText(err) }));
|
|
46685
46758
|
this.timers.push(setInterval(sync, SYNC_EVERY_MS));
|
|
46686
46759
|
void ensureSkill(companyId).catch((err) => warn("could not install the Mattermost skill", { err: errText(err) }));
|
|
46687
46760
|
if (cards) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperclip-plugin-mattermost",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "Paperclip agents in Mattermost: a bot per agent, live progress, files, approval and decision buttons, and per-person access, for each Paperclip company.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|