ftown-bridge 0.19.17 → 0.19.18
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 +40 -7
- 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();
|
|
@@ -117,8 +122,9 @@ export function registerFtownOpencodePlugin(client, options = {}) {
|
|
|
117
122
|
const messages = await drainMail();
|
|
118
123
|
if (messages.length === 0 || !currentSessionId) return;
|
|
119
124
|
const formatted = messages.map(formatMail).join('\n');
|
|
120
|
-
// Submitting a prompt starts a new turn; when it finishes,
|
|
121
|
-
// fires
|
|
125
|
+
// Submitting a prompt starts a new turn; when it finishes, the next
|
|
126
|
+
// idle boundary fires and any mail that arrived meanwhile is delivered
|
|
127
|
+
// then.
|
|
122
128
|
await client.session.prompt({
|
|
123
129
|
path: { id: currentSessionId },
|
|
124
130
|
body: {
|
|
@@ -134,15 +140,35 @@ export function registerFtownOpencodePlugin(client, options = {}) {
|
|
|
134
140
|
});
|
|
135
141
|
}
|
|
136
142
|
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Extract the opencode SESSION id from event properties. `info.id` is only
|
|
145
|
+
* trustworthy on session.created — on message.updated it is a MESSAGE id
|
|
146
|
+
* (msg_…), which must never be stored as the resume id.
|
|
147
|
+
*/
|
|
148
|
+
function trackSessionId(properties, preferInfoId = false) {
|
|
149
|
+
const id = properties?.sessionID
|
|
150
|
+
?? properties?.info?.sessionID
|
|
151
|
+
?? (preferInfoId ? properties?.info?.id : undefined);
|
|
139
152
|
if (typeof id === 'string' && id.trim()) currentSessionId = id.trim();
|
|
140
153
|
}
|
|
141
154
|
|
|
155
|
+
/** Turn boundary: mark idle, then drain and inject pending mail. */
|
|
156
|
+
function endTurn(properties) {
|
|
157
|
+
trackSessionId(properties);
|
|
158
|
+
const run = turnEndChain.then(async () => {
|
|
159
|
+
await postHook('Stop');
|
|
160
|
+
await deliverMail();
|
|
161
|
+
}).catch(() => {
|
|
162
|
+
// A failed hook POST or mail delivery must never crash the agent TUI.
|
|
163
|
+
});
|
|
164
|
+
turnEndChain = run;
|
|
165
|
+
return run;
|
|
166
|
+
}
|
|
167
|
+
|
|
142
168
|
async function handleEvent(event) {
|
|
143
169
|
switch (event.type) {
|
|
144
170
|
case 'session.created':
|
|
145
|
-
trackSessionId(event.properties);
|
|
171
|
+
trackSessionId(event.properties, true);
|
|
146
172
|
await postHook('SessionStart');
|
|
147
173
|
break;
|
|
148
174
|
case 'message.updated': {
|
|
@@ -152,10 +178,17 @@ export function registerFtownOpencodePlugin(client, options = {}) {
|
|
|
152
178
|
break;
|
|
153
179
|
}
|
|
154
180
|
case 'session.idle':
|
|
181
|
+
await endTurn(event.properties);
|
|
182
|
+
break;
|
|
183
|
+
case 'session.status': {
|
|
155
184
|
trackSessionId(event.properties);
|
|
156
|
-
|
|
157
|
-
|
|
185
|
+
// v1.18.x fires session.status(busy/idle) reliably; session.idle may
|
|
186
|
+
// not reach plugins at all. Busy keeps the mail pump from nudging
|
|
187
|
+
// mid-turn; idle is a full turn boundary.
|
|
188
|
+
if (event.properties?.status?.type === 'busy') await postHook('PreToolUse');
|
|
189
|
+
else if (event.properties?.status?.type === 'idle') await endTurn(event.properties);
|
|
158
190
|
break;
|
|
191
|
+
}
|
|
159
192
|
case 'session.error':
|
|
160
193
|
trackSessionId(event.properties);
|
|
161
194
|
await postHook('SessionEnd', { reason: 'error' });
|