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.
@@ -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, session.idle
121
- // fires again and any mail that arrived meanwhile is delivered then.
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
- function trackSessionId(properties) {
138
- const id = properties?.info?.id ?? properties?.sessionID ?? properties?.info?.sessionID;
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
- await postHook('Stop');
157
- await deliverMail();
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' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ftown-bridge",
3
- "version": "0.19.17",
3
+ "version": "0.19.18",
4
4
  "description": "CLI bridge for ftown — generic PTY-over-Centrifugo relay",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",