@agfpd/iapeer 0.2.70 → 0.2.71

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.2.70",
3
+ "version": "0.2.71",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -174,4 +174,40 @@ d('supervisor daemon (Bun-native pty, tick runtime)', () => {
174
174
  }
175
175
  }
176
176
  })
177
+
178
+ // FINDING-2 — restore the SERVE geometry on last detach. The daemon resizes the child pty to a
179
+ // client's size on attach and (before this) never restored it, so an unattached session stuck at a
180
+ // departed client's size → the fixed-geometry warm-deliver/ready-gate viewport model mismatched the
181
+ // pty. Drive it with a child that prints its live pty winsize ("ROWS COLS") to the pane-log: attach
182
+ // small → pty shrinks; detach → pty restores to the serve geometry.
183
+ test('FINDING-2: last detach restores the serve geometry (no sticking at the client size)', async () => {
184
+ const gSession = 'ptsgeo'
185
+ const paneLog = join(runDir, 'geo-pane.log')
186
+ const lastSize = (): string => {
187
+ const t = existsSync(paneLog) ? readFileSync(paneLog, 'utf8') : ''
188
+ const m = [...t.matchAll(/(\d+) (\d+)/g)]
189
+ return m.length ? `${m[m.length - 1][1]}x${m[m.length - 1][2]}` : ''
190
+ }
191
+ try {
192
+ const r = startSupervisorDaemon({
193
+ session: gSession,
194
+ runtime: 'tick',
195
+ runDir,
196
+ // serve geometry 200x50 (distinct from defaults); child reports its live pty winsize as "rows cols"
197
+ serve: { argv: ['sh', '-c', 'while :; do stty size 2>/dev/null; sleep 0.3; done'], env: { PATH: process.env.PATH ?? '' }, cwd: runDir, paneLogPath: paneLog, cols: 200, rows: 50 },
198
+ })
199
+ expect(r.state).toBe('started')
200
+ expect(await waitFor(() => lastSize() === '50 200'.replace(' ', 'x'), 6000)).toBe(true) // at serve geometry
201
+ const c = await attach(runDir, gSession, 90, 30) // attach at a DIFFERENT size → pty resizes to it
202
+ expect(await waitFor(() => lastSize() === '30x90', 6000)).toBe(true) // pty shrank to the client size
203
+ c.close() // last client gone
204
+ expect(await waitFor(() => lastSize() === '50x200', 6000)).toBe(true) // RESTORED to serve geometry
205
+ } finally {
206
+ try {
207
+ killSession(runDir, gSession)
208
+ } catch {
209
+ /* */
210
+ }
211
+ }
212
+ })
177
213
  })
@@ -90,6 +90,13 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
90
90
  const { session, runtime, runDir } = opts
91
91
  let cols = opts.cols ?? 80
92
92
  let rows = opts.rows ?? 30
93
+ // The SERVE geometry — the size the session is hosted at (HOST_COLS×HOST_ROWS for a pty-host peer),
94
+ // immutable. `cols`/`rows` above track the CURRENT (client-driven) size. A client attaches at its own
95
+ // terminal size and the pty resizes to match; on LAST detach we restore the serve geometry (below) so
96
+ // the unattached session never sticks at a departed client's size — otherwise the warm-deliver /
97
+ // ready-gate viewport model (built at the fixed serve geometry) mismatches the actually-resized pty.
98
+ const serveCols = cols
99
+ const serveRows = rows
93
100
  const sock = sockPath(runDir, session)
94
101
  const pid = pidPath(runDir, session)
95
102
 
@@ -217,28 +224,40 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
217
224
  sendClient(s, frame(FRAME_DATA, Buffer.from(snap, 'utf8')))
218
225
  }
219
226
 
227
+ // Resize the model + child pty to (c, r). Shared by client FRAME_RESIZE and the last-detach restore.
228
+ const applyGeometry = (c: number, r: number): void => {
229
+ cols = c
230
+ rows = r
231
+ try {
232
+ xterm.resize(c, r)
233
+ child.terminal.resize(c, r)
234
+ } catch {
235
+ /* transient */
236
+ }
237
+ // Best-effort SIGWINCH nudge for a SIGWINCH-reactive child (forces a repaint at the new size); NOT
238
+ // load-bearing for an attached client (the complete snapshot repaint is), but for the unattached
239
+ // restore it is what makes the child redraw into the pane-log at the serve geometry.
240
+ try {
241
+ child.terminal.resize(c, Math.max(1, r - 1))
242
+ child.terminal.resize(c, r)
243
+ } catch {
244
+ /* */
245
+ }
246
+ }
247
+ // Restore the serve geometry when the last client detaches, so the unattached session never sticks at
248
+ // a departed client's size (FINDING-2 — would mismatch the fixed-geometry warm-deliver/ready-gate
249
+ // viewport model). No-op when already at serve geometry or a client is still attached.
250
+ const restoreServeGeometryIfIdle = (): void => {
251
+ if (clients.size === 0 && (cols !== serveCols || rows !== serveRows)) applyGeometry(serveCols, serveRows)
252
+ }
253
+
220
254
  const onClientFrame = (s: { write(b: Buffer): number }, type: number, payload: Buffer): void => {
221
255
  if (type === FRAME_DATA) {
222
256
  child.terminal.write(payload) // keystrokes → child
223
257
  } else if (type === FRAME_RESIZE) {
224
258
  const sz = parseSize(payload)
225
- cols = sz.cols
226
- rows = sz.rows
227
- try {
228
- xterm.resize(cols, rows)
229
- child.terminal.resize(cols, rows)
230
- } catch {
231
- /* transient */
232
- }
259
+ applyGeometry(sz.cols, sz.rows)
233
260
  repaint(s) // instant paint from the snapshot (delivered COMPLETE via backpressure)
234
- // Best-effort SIGWINCH nudge for a SIGWINCH-reactive child; NOT load-bearing (the complete
235
- // snapshot above is the real freshness fix).
236
- try {
237
- child.terminal.resize(cols, Math.max(1, rows - 1))
238
- child.terminal.resize(cols, rows)
239
- } catch {
240
- /* */
241
- }
242
261
  }
243
262
  }
244
263
 
@@ -266,11 +285,13 @@ export function runSupervisorDaemon(opts: SupervisorDaemonOptions): void {
266
285
  clients.delete(s)
267
286
  writers.delete(s)
268
287
  syncAttached()
288
+ restoreServeGeometryIfIdle() // last client gone → un-stick the pty from its size
269
289
  },
270
290
  error(s: object) {
271
291
  clients.delete(s)
272
292
  writers.delete(s)
273
293
  syncAttached()
294
+ restoreServeGeometryIfIdle()
274
295
  },
275
296
  },
276
297
  })