backeer-opencode 0.2.2 → 0.2.4
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/CHANGELOG.md +22 -0
- package/README.md +3 -2
- package/dist/backeer-permission.js +390 -172
- package/package.json +7 -3
- package/src/delivery-tracker.js +75 -0
- package/src/feedback-session-store.js +110 -0
- package/src/lifecycle-publishers.js +2 -0
- package/src/permission-command.js +60 -0
- package/src/permission-controller.js +14 -57
- package/src/plugin.js +33 -23
- package/src/session-messages.js +75 -84
- package/src/sse.js +99 -43
- package/src/updates.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backeer-opencode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Backeer lifecycle notifications and remote permission decisions for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "esbuild src/index.js --bundle --platform=node --format=esm --target=node18 --outfile=dist/backeer-permission.js --legal-comments=none",
|
|
23
|
-
"check": "node --check bin/backeer-opencode.js && npm test",
|
|
23
|
+
"check": "npm run lint && node --check bin/backeer-opencode.js && npm test",
|
|
24
|
+
"lint": "eslint src bin test eslint.config.js --max-warnings=0",
|
|
24
25
|
"pretest": "npm run build",
|
|
25
26
|
"test": "node --test",
|
|
26
27
|
"pack:check": "npm pack --dry-run",
|
|
@@ -50,7 +51,10 @@
|
|
|
50
51
|
"access": "public"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"
|
|
54
|
+
"@eslint/js": "^9.39.5",
|
|
55
|
+
"esbuild": "^0.28.2",
|
|
56
|
+
"eslint": "^9.39.5",
|
|
57
|
+
"globals": "^16.5.0"
|
|
54
58
|
},
|
|
55
59
|
"license": "SEE LICENSE IN LICENSE"
|
|
56
60
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { rememberBounded } from "./utilities.js"
|
|
2
|
+
|
|
3
|
+
export function createDeliveryTracker() {
|
|
4
|
+
const states = new Map()
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
begin(key) {
|
|
8
|
+
if (states.has(key)) return false
|
|
9
|
+
rememberBounded(states, key, "publishing")
|
|
10
|
+
return true
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
succeed(key) {
|
|
14
|
+
if (states.get(key) === "publishing") {
|
|
15
|
+
rememberBounded(states, key, "published")
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
fail(key) {
|
|
20
|
+
if (states.get(key) === "publishing") states.delete(key)
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
clear() {
|
|
24
|
+
states.clear()
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createCompletionTracker() {
|
|
30
|
+
const states = new Map()
|
|
31
|
+
|
|
32
|
+
function stateFor(key) {
|
|
33
|
+
let state = states.get(key)
|
|
34
|
+
if (!state) {
|
|
35
|
+
state = { complete: false, publishing: false, revision: 0 }
|
|
36
|
+
rememberBounded(states, key, state)
|
|
37
|
+
}
|
|
38
|
+
return state
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
update(key, complete) {
|
|
43
|
+
const state = stateFor(key)
|
|
44
|
+
if (!complete) {
|
|
45
|
+
state.complete = false
|
|
46
|
+
state.publishing = false
|
|
47
|
+
state.revision++
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
if (state.complete || state.publishing) return
|
|
51
|
+
|
|
52
|
+
state.publishing = true
|
|
53
|
+
state.revision++
|
|
54
|
+
return { key, revision: state.revision }
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
succeed(attempt) {
|
|
58
|
+
const state = states.get(attempt?.key)
|
|
59
|
+
if (!state || state.revision !== attempt.revision) return
|
|
60
|
+
state.complete = true
|
|
61
|
+
state.publishing = false
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
fail(attempt) {
|
|
65
|
+
const state = states.get(attempt?.key)
|
|
66
|
+
if (!state || state.revision !== attempt.revision) return
|
|
67
|
+
state.complete = false
|
|
68
|
+
state.publishing = false
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
clear() {
|
|
72
|
+
states.clear()
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { limit, rememberBounded } from "./utilities.js"
|
|
2
|
+
|
|
3
|
+
const MAX_QUEUED_MESSAGES = 20
|
|
4
|
+
|
|
5
|
+
export class FeedbackSessionStore {
|
|
6
|
+
constructor({ send, reporter }) {
|
|
7
|
+
this.send = send
|
|
8
|
+
this.reporter = reporter
|
|
9
|
+
this.sessions = new Map()
|
|
10
|
+
this.disposed = false
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
stateFor(sessionID) {
|
|
14
|
+
let state = this.sessions.get(sessionID)
|
|
15
|
+
if (!state) {
|
|
16
|
+
state = {
|
|
17
|
+
idle: false,
|
|
18
|
+
flushing: false,
|
|
19
|
+
blockers: new Set(),
|
|
20
|
+
queue: [],
|
|
21
|
+
queued: new Set(),
|
|
22
|
+
queueOverflowReported: false,
|
|
23
|
+
}
|
|
24
|
+
rememberBounded(this.sessions, sessionID, state)
|
|
25
|
+
}
|
|
26
|
+
return state
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
hasReadyMessage(state) {
|
|
30
|
+
return !this.disposed && state.idle &&
|
|
31
|
+
state.blockers.size === 0 && state.queue.length > 0
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async flush(sessionID, state) {
|
|
35
|
+
if (state.flushing || !this.hasReadyMessage(state)) return
|
|
36
|
+
state.flushing = true
|
|
37
|
+
try {
|
|
38
|
+
while (this.hasReadyMessage(state)) {
|
|
39
|
+
const item = state.queue[0]
|
|
40
|
+
try {
|
|
41
|
+
await this.send(sessionID, item.kind, item.message)
|
|
42
|
+
state.queue.shift()
|
|
43
|
+
state.queued.delete(item.key)
|
|
44
|
+
} catch (error) {
|
|
45
|
+
state.idle = false
|
|
46
|
+
this.reporter.debug("Conversation feedback could not be displayed", error)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} finally {
|
|
50
|
+
state.flushing = false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
enqueue(kind, sessionID, message) {
|
|
55
|
+
if (this.disposed || typeof sessionID !== "string" || !sessionID || !message) return
|
|
56
|
+
const state = this.stateFor(sessionID)
|
|
57
|
+
const key = `${sessionID}:${kind}:${message}`
|
|
58
|
+
if (state.queued.has(key)) return
|
|
59
|
+
|
|
60
|
+
state.queue.push({ kind, message: limit(message, 300), key })
|
|
61
|
+
state.queued.add(key)
|
|
62
|
+
if (state.queue.length > MAX_QUEUED_MESSAGES) {
|
|
63
|
+
const removed = state.queue.shift()
|
|
64
|
+
state.queued.delete(removed.key)
|
|
65
|
+
if (!state.queueOverflowReported) {
|
|
66
|
+
state.queueOverflowReported = true
|
|
67
|
+
this.reporter.warning("Conversation feedback queue full — oldest message discarded")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (state.idle && state.blockers.size === 0) void this.flush(sessionID, state)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setIdle(sessionID, idle) {
|
|
74
|
+
if (typeof sessionID !== "string" || !sessionID) return
|
|
75
|
+
const state = this.stateFor(sessionID)
|
|
76
|
+
state.idle = idle
|
|
77
|
+
if (idle) void this.flush(sessionID, state)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
beginInteraction(sessionID, key) {
|
|
81
|
+
const state = this.stateFor(sessionID)
|
|
82
|
+
state.idle = false
|
|
83
|
+
state.blockers.add(key)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
endInteraction(sessionID, key, kind) {
|
|
87
|
+
const state = this.stateFor(sessionID)
|
|
88
|
+
if (!state.blockers.delete(key)) {
|
|
89
|
+
const fallback = [...state.blockers]
|
|
90
|
+
.find((blocker) => blocker.startsWith(`${kind}:`))
|
|
91
|
+
if (fallback) state.blockers.delete(fallback)
|
|
92
|
+
}
|
|
93
|
+
state.idle = false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
failSession(sessionID) {
|
|
97
|
+
const state = this.stateFor(sessionID)
|
|
98
|
+
state.blockers.clear()
|
|
99
|
+
state.idle = false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
delete(sessionID) {
|
|
103
|
+
this.sessions.delete(sessionID)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
dispose() {
|
|
107
|
+
this.disposed = true
|
|
108
|
+
this.sessions.clear()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { postJsonWithRetry } from "./http.js"
|
|
2
2
|
import { limit, stableHash } from "./utilities.js"
|
|
3
3
|
|
|
4
|
+
// Intentional sanitization of non-printable control bytes before logging.
|
|
5
|
+
// eslint-disable-next-line no-control-regex
|
|
4
6
|
const CONTROL_CHARACTERS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g
|
|
5
7
|
const SECRET_ASSIGNMENT = /\b(token|secret|password|api[-_ ]?key)\s*[:=]\s*[^\s,;]+/gi
|
|
6
8
|
const BEARER_TOKEN = /\bBearer\s+[^\s,;]+/gi
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { HttpError } from "./http.js"
|
|
2
|
+
|
|
3
|
+
export const ALLOWED_DECISIONS = new Set(["once", "always", "reject"])
|
|
4
|
+
|
|
5
|
+
const DECISION_LABELS = {
|
|
6
|
+
once: "Allow once",
|
|
7
|
+
always: "Always allow",
|
|
8
|
+
reject: "Reject",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function validatePermissionCommand(command, now = Date.now()) {
|
|
12
|
+
if (!ALLOWED_DECISIONS.has(command.decision)) {
|
|
13
|
+
return {
|
|
14
|
+
status: "rejected",
|
|
15
|
+
decision: command.decision,
|
|
16
|
+
errorCode: "invalid_decision",
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const expiresAt = Date.parse(command.expiresAt)
|
|
21
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
22
|
+
return { status: "expired", decision: command.decision }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (command.decision === "always" && command.confirmed !== true) {
|
|
26
|
+
return {
|
|
27
|
+
status: "rejected",
|
|
28
|
+
decision: command.decision,
|
|
29
|
+
errorCode: "confirmation_required",
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function failureAcknowledgement(command, error) {
|
|
35
|
+
const stale = error instanceof HttpError && error.status === 404
|
|
36
|
+
return {
|
|
37
|
+
status: stale ? "stale" : "failed",
|
|
38
|
+
decision: command.decision,
|
|
39
|
+
errorCode: stale ? "permission_not_found" : "opencode_reply_failed",
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function reportAcknowledgement(acknowledgement, pending, reporter, conversation) {
|
|
44
|
+
const messages = {
|
|
45
|
+
applied: `Decision applied: ${DECISION_LABELS[acknowledgement.decision]}`,
|
|
46
|
+
expired: "Decision expired before it could be applied",
|
|
47
|
+
stale: "Permission was already answered locally",
|
|
48
|
+
rejected: "Decision was rejected by validation",
|
|
49
|
+
}
|
|
50
|
+
const message = messages[acknowledgement.status]
|
|
51
|
+
if (!message) return
|
|
52
|
+
|
|
53
|
+
if (acknowledgement.status === "applied") {
|
|
54
|
+
reporter.applied(message)
|
|
55
|
+
conversation?.applied(pending.sessionID, message)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
reporter.warning(message)
|
|
59
|
+
conversation?.warning(pending.sessionID, message)
|
|
60
|
+
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { HttpError, postJsonWithRetry } from "./http.js"
|
|
2
|
+
import {
|
|
3
|
+
failureAcknowledgement,
|
|
4
|
+
reportAcknowledgement,
|
|
5
|
+
validatePermissionCommand,
|
|
6
|
+
} from "./permission-command.js"
|
|
2
7
|
import { runCommandStream } from "./sse.js"
|
|
3
8
|
import { rememberBounded } from "./utilities.js"
|
|
4
9
|
|
|
5
|
-
const ALLOWED_DECISIONS = new Set(["once", "always", "reject"])
|
|
6
|
-
const DECISION_LABELS = {
|
|
7
|
-
once: "Allow once",
|
|
8
|
-
always: "Always allow",
|
|
9
|
-
reject: "Reject",
|
|
10
|
-
}
|
|
11
|
-
|
|
12
10
|
async function replyToOpenCode(client, pending, decision) {
|
|
13
11
|
const result = await client.postSessionIdPermissionsPermissionId({
|
|
14
12
|
path: { id: pending.sessionID, permissionID: pending.requestID },
|
|
@@ -41,7 +39,6 @@ export function createPermissionController({
|
|
|
41
39
|
const externalIDByRequestID = new Map()
|
|
42
40
|
const processingCommands = new Set()
|
|
43
41
|
const completedCommands = new Map()
|
|
44
|
-
const publishedPermissions = new Map()
|
|
45
42
|
let commandStream
|
|
46
43
|
let disposed = false
|
|
47
44
|
|
|
@@ -74,36 +71,18 @@ export function createPermissionController({
|
|
|
74
71
|
if (!pending) return
|
|
75
72
|
|
|
76
73
|
processingCommands.add(command.id)
|
|
77
|
-
let acknowledgement
|
|
74
|
+
let acknowledgement = validatePermissionCommand(command)
|
|
78
75
|
try {
|
|
79
|
-
if (!
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) {
|
|
84
|
-
acknowledgement = { status: "expired", decision: command.decision }
|
|
85
|
-
} else if (command.decision === "always" && command.confirmed !== true) {
|
|
86
|
-
acknowledgement = {
|
|
87
|
-
status: "rejected",
|
|
88
|
-
decision: command.decision,
|
|
89
|
-
errorCode: "confirmation_required",
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
await replyToOpenCode(client, pending, command.decision)
|
|
93
|
-
acknowledgement = { status: "applied", decision: command.decision }
|
|
94
|
-
removePendingByRequestID(pending.requestID)
|
|
95
|
-
}
|
|
76
|
+
if (!acknowledgement) {
|
|
77
|
+
await replyToOpenCode(client, pending, command.decision)
|
|
78
|
+
acknowledgement = { status: "applied", decision: command.decision }
|
|
79
|
+
removePendingByRequestID(pending.requestID)
|
|
96
80
|
}
|
|
97
81
|
} catch (error) {
|
|
98
|
-
|
|
99
|
-
acknowledgement
|
|
100
|
-
|
|
101
|
-
decision: ALLOWED_DECISIONS.has(command.decision)
|
|
102
|
-
? command.decision
|
|
103
|
-
: undefined,
|
|
104
|
-
errorCode: stale ? "permission_not_found" : "opencode_reply_failed",
|
|
82
|
+
acknowledgement = failureAcknowledgement(command, error)
|
|
83
|
+
if (acknowledgement.status === "stale") {
|
|
84
|
+
removePendingByRequestID(pending.requestID)
|
|
105
85
|
}
|
|
106
|
-
if (stale) removePendingByRequestID(pending.requestID)
|
|
107
86
|
reporter.error("Decision could not be applied", error)
|
|
108
87
|
conversation?.error(pending.sessionID, "Decision could not be applied")
|
|
109
88
|
} finally {
|
|
@@ -118,24 +97,7 @@ export function createPermissionController({
|
|
|
118
97
|
}
|
|
119
98
|
rememberBounded(completedCommands, command.id, acknowledgement)
|
|
120
99
|
|
|
121
|
-
|
|
122
|
-
reporter.applied(
|
|
123
|
-
`Decision applied: ${DECISION_LABELS[acknowledgement.decision]}`,
|
|
124
|
-
)
|
|
125
|
-
conversation?.applied(
|
|
126
|
-
pending.sessionID,
|
|
127
|
-
`Decision applied: ${DECISION_LABELS[acknowledgement.decision]}`,
|
|
128
|
-
)
|
|
129
|
-
} else if (acknowledgement.status === "expired") {
|
|
130
|
-
reporter.warning("Decision expired before it could be applied")
|
|
131
|
-
conversation?.warning(pending.sessionID, "Decision expired before it could be applied")
|
|
132
|
-
} else if (acknowledgement.status === "stale") {
|
|
133
|
-
reporter.warning("Permission was already answered locally")
|
|
134
|
-
conversation?.warning(pending.sessionID, "Permission was already answered locally")
|
|
135
|
-
} else if (acknowledgement.status === "rejected") {
|
|
136
|
-
reporter.warning("Decision was rejected by validation")
|
|
137
|
-
conversation?.warning(pending.sessionID, "Decision was rejected by validation")
|
|
138
|
-
}
|
|
100
|
+
reportAcknowledgement(acknowledgement, pending, reporter, conversation)
|
|
139
101
|
|
|
140
102
|
await acknowledgeCommand(config, command.id, acknowledgement).catch((error) => {
|
|
141
103
|
reporter.warning("Decision receipt could not be sent", error)
|
|
@@ -167,10 +129,6 @@ export function createPermissionController({
|
|
|
167
129
|
pendingByExternalID.set(identity.externalId, { ...identity, directory })
|
|
168
130
|
externalIDByRequestID.set(identity.requestID, identity.externalId)
|
|
169
131
|
ensureCommandStream()
|
|
170
|
-
|
|
171
|
-
if (publishedPermissions.has(identity.externalId)) return false
|
|
172
|
-
rememberBounded(publishedPermissions, identity.externalId, true)
|
|
173
|
-
return true
|
|
174
132
|
},
|
|
175
133
|
|
|
176
134
|
resolveLocally(requestID) {
|
|
@@ -198,7 +156,6 @@ export function createPermissionController({
|
|
|
198
156
|
externalIDByRequestID.clear()
|
|
199
157
|
processingCommands.clear()
|
|
200
158
|
completedCommands.clear()
|
|
201
|
-
publishedPermissions.clear()
|
|
202
159
|
},
|
|
203
160
|
}
|
|
204
161
|
}
|
package/src/plugin.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { loadConfig } from "./config.js"
|
|
2
|
+
import {
|
|
3
|
+
createCompletionTracker,
|
|
4
|
+
createDeliveryTracker,
|
|
5
|
+
} from "./delivery-tracker.js"
|
|
2
6
|
import {
|
|
3
7
|
permissionIdentity,
|
|
4
8
|
publishPermission,
|
|
@@ -18,7 +22,6 @@ import {
|
|
|
18
22
|
createUpdateChecker,
|
|
19
23
|
createUpdateNoticeHook,
|
|
20
24
|
} from "./updates.js"
|
|
21
|
-
import { rememberBounded } from "./utilities.js"
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
27
|
* OpenCode server plugin entrypoint.
|
|
@@ -58,8 +61,9 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
58
61
|
directory,
|
|
59
62
|
conversation,
|
|
60
63
|
})
|
|
61
|
-
const
|
|
62
|
-
const
|
|
64
|
+
const permissionPublications = createDeliveryTracker()
|
|
65
|
+
const questionPublications = createDeliveryTracker()
|
|
66
|
+
const todoCompletions = createCompletionTracker()
|
|
63
67
|
const updateChecker = createUpdateChecker()
|
|
64
68
|
const handleBackeerCommand = createBackeerCommandHandler({
|
|
65
69
|
client,
|
|
@@ -83,7 +87,8 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
83
87
|
reporter.error("Permission event is missing correlation IDs")
|
|
84
88
|
return
|
|
85
89
|
}
|
|
86
|
-
|
|
90
|
+
permissionController.register(identity)
|
|
91
|
+
if (!permissionPublications.begin(identity.externalId)) return
|
|
87
92
|
|
|
88
93
|
void publishPermission(
|
|
89
94
|
config,
|
|
@@ -92,10 +97,12 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
92
97
|
directory,
|
|
93
98
|
identity,
|
|
94
99
|
conversation,
|
|
95
|
-
).
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
).then(() => permissionPublications.succeed(identity.externalId))
|
|
101
|
+
.catch((error) => {
|
|
102
|
+
permissionPublications.fail(identity.externalId)
|
|
103
|
+
reporter.error("Permission request could not be sent", error)
|
|
104
|
+
conversation.error(identity.sessionID, "Permission request could not be sent")
|
|
105
|
+
})
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
async function handleTodoUpdated(event) {
|
|
@@ -107,9 +114,8 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
107
114
|
|
|
108
115
|
const isComplete = todos.length > 0 &&
|
|
109
116
|
todos.every((todo) => todo?.status === "completed")
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
if (!isComplete || wasComplete) return
|
|
117
|
+
const attempt = todoCompletions.update(sessionID, isComplete)
|
|
118
|
+
if (!attempt) return
|
|
113
119
|
|
|
114
120
|
void publishTodoCompletion(
|
|
115
121
|
config,
|
|
@@ -119,10 +125,12 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
119
125
|
client,
|
|
120
126
|
directory,
|
|
121
127
|
conversation,
|
|
122
|
-
).
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
).then(() => todoCompletions.succeed(attempt))
|
|
129
|
+
.catch((error) => {
|
|
130
|
+
todoCompletions.fail(attempt)
|
|
131
|
+
reporter.error("Todo completion could not be sent", error)
|
|
132
|
+
conversation.error(sessionID, "Todo completion could not be sent")
|
|
133
|
+
})
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
async function handleQuestionAsked(event) {
|
|
@@ -131,8 +139,7 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
131
139
|
reporter.error("Question event is missing correlation IDs")
|
|
132
140
|
return
|
|
133
141
|
}
|
|
134
|
-
if (
|
|
135
|
-
rememberBounded(publishedQuestions, identity.externalId, true)
|
|
142
|
+
if (!questionPublications.begin(identity.externalId)) return
|
|
136
143
|
|
|
137
144
|
void publishQuestion(
|
|
138
145
|
config,
|
|
@@ -142,10 +149,12 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
142
149
|
client,
|
|
143
150
|
directory,
|
|
144
151
|
conversation,
|
|
145
|
-
).
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
).then(() => questionPublications.succeed(identity.externalId))
|
|
153
|
+
.catch((error) => {
|
|
154
|
+
questionPublications.fail(identity.externalId)
|
|
155
|
+
reporter.error("Question could not be sent", error)
|
|
156
|
+
conversation.error(identity.sessionID, "Question could not be sent")
|
|
157
|
+
})
|
|
149
158
|
}
|
|
150
159
|
|
|
151
160
|
return {
|
|
@@ -191,8 +200,9 @@ export default async function BackeerPlugin({ client, directory }, options = {})
|
|
|
191
200
|
dispose: async () => {
|
|
192
201
|
await permissionController.dispose()
|
|
193
202
|
lifecycleController.dispose()
|
|
194
|
-
|
|
195
|
-
|
|
203
|
+
permissionPublications.clear()
|
|
204
|
+
questionPublications.clear()
|
|
205
|
+
todoCompletions.clear()
|
|
196
206
|
conversation.dispose()
|
|
197
207
|
},
|
|
198
208
|
}
|