ftown-bridge 0.19.17 → 0.19.19
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/opencode-plugin/ftown.js +51 -11
- package/package.json +1 -1
package/opencode-plugin/ftown.js
CHANGED
|
@@ -65,6 +65,11 @@ export function registerFtownOpencodePlugin(client, options = {}) {
|
|
|
65
65
|
// The opencode session id this plugin instance tracks — updated from every
|
|
66
66
|
// event that carries one, so it stays correct across TUI session switches.
|
|
67
67
|
let currentSessionId;
|
|
68
|
+
// Serializes turn-end handling: opencode may emit BOTH session.status(idle)
|
|
69
|
+
// and session.idle for the same boundary. Unordered, two concurrent drains
|
|
70
|
+
// could read the same undelivered mail before either marks it delivered and
|
|
71
|
+
// inject it twice.
|
|
72
|
+
let turnEndChain = Promise.resolve();
|
|
68
73
|
|
|
69
74
|
async function request(path, init = {}) {
|
|
70
75
|
const pointer = await readBridgePointer();
|
|
@@ -98,10 +103,10 @@ export function registerFtownOpencodePlugin(client, options = {}) {
|
|
|
98
103
|
});
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
async function
|
|
106
|
+
async function listInbox(peek) {
|
|
102
107
|
if (!ftownSessionId) return [];
|
|
103
108
|
const response = await request(
|
|
104
|
-
`/api/sessions/${encodeURIComponent(ftownSessionId)}/inbox?wait=0`,
|
|
109
|
+
`/api/sessions/${encodeURIComponent(ftownSessionId)}/inbox?wait=0${peek ? '&peek=1' : ''}`,
|
|
105
110
|
{ method: 'GET' },
|
|
106
111
|
);
|
|
107
112
|
if (!response) return [];
|
|
@@ -113,12 +118,18 @@ export function registerFtownOpencodePlugin(client, options = {}) {
|
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Peek first, and only mark delivered AFTER the prompt was accepted. If
|
|
123
|
+
* submission fails, mail stays queued for the next boundary instead of
|
|
124
|
+
* being silently lost between a marking drain and a failed injection.
|
|
125
|
+
*/
|
|
116
126
|
async function deliverMail() {
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
if (!currentSessionId) return;
|
|
128
|
+
const messages = await listInbox(true);
|
|
129
|
+
if (messages.length === 0) return;
|
|
119
130
|
const formatted = messages.map(formatMail).join('\n');
|
|
120
|
-
// Submitting a prompt starts a new turn; when it finishes,
|
|
121
|
-
// fires
|
|
131
|
+
// Submitting a prompt starts a new turn; when it finishes, the next idle
|
|
132
|
+
// boundary fires and any mail that arrived meanwhile is delivered then.
|
|
122
133
|
await client.session.prompt({
|
|
123
134
|
path: { id: currentSessionId },
|
|
124
135
|
body: {
|
|
@@ -132,17 +143,39 @@ export function registerFtownOpencodePlugin(client, options = {}) {
|
|
|
132
143
|
],
|
|
133
144
|
},
|
|
134
145
|
});
|
|
146
|
+
// Accepted — only now let the bridge mark the peeked messages delivered.
|
|
147
|
+
await listInbox(false);
|
|
135
148
|
}
|
|
136
149
|
|
|
137
|
-
|
|
138
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Extract the opencode SESSION id from event properties. `info.id` is only
|
|
152
|
+
* trustworthy on session.created — on message.updated it is a MESSAGE id
|
|
153
|
+
* (msg_…), which must never be stored as the resume id.
|
|
154
|
+
*/
|
|
155
|
+
function trackSessionId(properties, preferInfoId = false) {
|
|
156
|
+
const id = properties?.sessionID
|
|
157
|
+
?? properties?.info?.sessionID
|
|
158
|
+
?? (preferInfoId ? properties?.info?.id : undefined);
|
|
139
159
|
if (typeof id === 'string' && id.trim()) currentSessionId = id.trim();
|
|
140
160
|
}
|
|
141
161
|
|
|
162
|
+
/** Turn boundary: mark idle, then drain and inject pending mail. */
|
|
163
|
+
function endTurn(properties) {
|
|
164
|
+
trackSessionId(properties);
|
|
165
|
+
const run = turnEndChain.then(async () => {
|
|
166
|
+
await postHook('Stop');
|
|
167
|
+
await deliverMail();
|
|
168
|
+
}).catch(() => {
|
|
169
|
+
// A failed hook POST or mail delivery must never crash the agent TUI.
|
|
170
|
+
});
|
|
171
|
+
turnEndChain = run;
|
|
172
|
+
return run;
|
|
173
|
+
}
|
|
174
|
+
|
|
142
175
|
async function handleEvent(event) {
|
|
143
176
|
switch (event.type) {
|
|
144
177
|
case 'session.created':
|
|
145
|
-
trackSessionId(event.properties);
|
|
178
|
+
trackSessionId(event.properties, true);
|
|
146
179
|
await postHook('SessionStart');
|
|
147
180
|
break;
|
|
148
181
|
case 'message.updated': {
|
|
@@ -152,10 +185,17 @@ export function registerFtownOpencodePlugin(client, options = {}) {
|
|
|
152
185
|
break;
|
|
153
186
|
}
|
|
154
187
|
case 'session.idle':
|
|
188
|
+
await endTurn(event.properties);
|
|
189
|
+
break;
|
|
190
|
+
case 'session.status': {
|
|
155
191
|
trackSessionId(event.properties);
|
|
156
|
-
|
|
157
|
-
|
|
192
|
+
// v1.18.x fires session.status(busy/idle) reliably; session.idle may
|
|
193
|
+
// not reach plugins at all. Busy keeps the mail pump from nudging
|
|
194
|
+
// mid-turn; idle is a full turn boundary.
|
|
195
|
+
if (event.properties?.status?.type === 'busy') await postHook('PreToolUse');
|
|
196
|
+
else if (event.properties?.status?.type === 'idle') await endTurn(event.properties);
|
|
158
197
|
break;
|
|
198
|
+
}
|
|
159
199
|
case 'session.error':
|
|
160
200
|
trackSessionId(event.properties);
|
|
161
201
|
await postHook('SessionEnd', { reason: 'error' });
|