pi-link 0.1.9 → 0.1.10
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 +171 -159
- package/README.md +589 -580
- package/bin/pi-link.mjs +75 -65
- package/index.ts +1480 -1424
- package/package.json +1 -1
- package/skills/pi-link-coordination/SKILL.md +16 -9
package/README.md
CHANGED
|
@@ -1,580 +1,589 @@
|
|
|
1
|
-
# pi-link
|
|
2
|
-
|
|
3
|
-
A WebSocket-based inter-terminal communication system that creates a local network between multiple Pi coding agent terminals. Enables terminals to discover each other, exchange messages, and orchestrate work across agents - all automatically on `localhost`.
|
|
4
|
-
|
|
5
|
-
> Self-contained TypeScript in a single `index.ts` file. Start Pi with `--link` or
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Table of Contents
|
|
10
|
-
|
|
11
|
-
- [Why?](#why)
|
|
12
|
-
- [Prerequisites](#prerequisites)
|
|
13
|
-
- [Quick Start](#quick-start)
|
|
14
|
-
- [Walkthrough](#walkthrough)
|
|
15
|
-
- [Configuration](#configuration)
|
|
16
|
-
- [LLM Tools](#llm-tools)
|
|
17
|
-
- [Slash Commands](#slash-commands)
|
|
18
|
-
- [Architecture](#architecture)
|
|
19
|
-
- [Troubleshooting](#troubleshooting)
|
|
20
|
-
- [Limitations & Design Decisions](#limitations--design-decisions)
|
|
21
|
-
- [Dependencies](#dependencies)
|
|
22
|
-
- [Internals](#internals)
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## Why?
|
|
27
|
-
|
|
28
|
-
A single Pi terminal is powerful. Multiple terminals working together unlock new patterns:
|
|
29
|
-
|
|
30
|
-
- **Research + Build** - one terminal investigates APIs, docs, or logs while another writes code based on the findings.
|
|
31
|
-
- **Fan-out** - split a large task across agents (e.g., "terminal A handles the backend, terminal B handles the frontend") and collect results.
|
|
32
|
-
- **Orchestrator / Worker** - designate one terminal as a coordinator that delegates subtasks to others via `link_prompt` and assembles the final output.
|
|
33
|
-
- **Review pipeline** - one terminal writes code, another reviews it, back and forth until both are satisfied.
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## Prerequisites
|
|
38
|
-
|
|
39
|
-
- [Pi coding agent](https://github.com/
|
|
40
|
-
- Node.js (LTS recommended)
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## Quick Start
|
|
45
|
-
|
|
46
|
-
### Install
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
pi install npm:pi-link
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Uninstall
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
pi uninstall npm:pi-link
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Usage
|
|
59
|
-
|
|
60
|
-
Link is **off by default**. Start Pi with `--link
|
|
61
|
-
|
|
62
|
-
```
|
|
63
|
-
Terminal 1 Terminal 2
|
|
64
|
-
---------- ----------
|
|
65
|
-
$ pi --link
|
|
66
|
-
✓ Link hub started on :9900 as "
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
```
|
|
72
|
-
pi-link
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
cwd: ~/my-project
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
| `pi-link
|
|
141
|
-
|
|
|
142
|
-
| `/link-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
pi-link
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
-
|
|
161
|
-
|
|
162
|
-
- **
|
|
163
|
-
- **
|
|
164
|
-
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
|
208
|
-
|
|
|
209
|
-
| `
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
-
|
|
213
|
-
-
|
|
214
|
-
- **
|
|
215
|
-
- **
|
|
216
|
-
- **
|
|
217
|
-
-
|
|
218
|
-
-
|
|
219
|
-
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
|
231
|
-
|
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
|
257
|
-
|
|
|
258
|
-
| `/link`
|
|
259
|
-
| `/link-
|
|
260
|
-
| `/link-
|
|
261
|
-
| `/link-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
-
|
|
321
|
-
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
-
|
|
353
|
-
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
-
|
|
359
|
-
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
|
371
|
-
|
|
|
372
|
-
|
|
|
373
|
-
|
|
|
374
|
-
|
|
|
375
|
-
|
|
|
376
|
-
|
|
|
377
|
-
|
|
|
378
|
-
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
|
388
|
-
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
|
394
|
-
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
|
400
|
-
|
|
|
401
|
-
| `@mariozechner/pi-
|
|
402
|
-
| `@
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
"
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
"
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
|
439
|
-
|
|
|
440
|
-
| `
|
|
441
|
-
| `
|
|
442
|
-
| `
|
|
443
|
-
| `
|
|
444
|
-
| `
|
|
445
|
-
| `
|
|
446
|
-
| `
|
|
447
|
-
| `
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
|
457
|
-
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
|
461
|
-
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
|
477
|
-
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
|
489
|
-
|
|
|
490
|
-
|
|
|
491
|
-
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
-
|
|
507
|
-
- On
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
|
515
|
-
|
|
|
516
|
-
| `
|
|
517
|
-
| `
|
|
518
|
-
| `
|
|
519
|
-
| `
|
|
520
|
-
| `
|
|
521
|
-
| `
|
|
522
|
-
| `
|
|
523
|
-
| `
|
|
524
|
-
| `
|
|
525
|
-
| `
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
- **
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
- **`
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1
|
+
# pi-link
|
|
2
|
+
|
|
3
|
+
A WebSocket-based inter-terminal communication system that creates a local network between multiple Pi coding agent terminals. Enables terminals to discover each other, exchange messages, and orchestrate work across agents - all automatically on `localhost`.
|
|
4
|
+
|
|
5
|
+
> Self-contained TypeScript in a single `index.ts` file. Start Pi with `--link` to enable, or use `pi-link <name>` to resume/create named sessions
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Why?](#why)
|
|
12
|
+
- [Prerequisites](#prerequisites)
|
|
13
|
+
- [Quick Start](#quick-start)
|
|
14
|
+
- [Walkthrough](#walkthrough)
|
|
15
|
+
- [Configuration](#configuration)
|
|
16
|
+
- [LLM Tools](#llm-tools)
|
|
17
|
+
- [Slash Commands](#slash-commands)
|
|
18
|
+
- [Architecture](#architecture)
|
|
19
|
+
- [Troubleshooting](#troubleshooting)
|
|
20
|
+
- [Limitations & Design Decisions](#limitations--design-decisions)
|
|
21
|
+
- [Dependencies](#dependencies)
|
|
22
|
+
- [Internals](#internals)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Why?
|
|
27
|
+
|
|
28
|
+
A single Pi terminal is powerful. Multiple terminals working together unlock new patterns:
|
|
29
|
+
|
|
30
|
+
- **Research + Build** - one terminal investigates APIs, docs, or logs while another writes code based on the findings.
|
|
31
|
+
- **Fan-out** - split a large task across agents (e.g., "terminal A handles the backend, terminal B handles the frontend") and collect results.
|
|
32
|
+
- **Orchestrator / Worker** - designate one terminal as a coordinator that delegates subtasks to others via `link_prompt` and assembles the final output.
|
|
33
|
+
- **Review pipeline** - one terminal writes code, another reviews it, back and forth until both are satisfied.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Prerequisites
|
|
38
|
+
|
|
39
|
+
- [Pi coding agent](https://github.com/badlogic/pi-mono) installed and working
|
|
40
|
+
- Node.js (LTS recommended)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
### Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pi install npm:pi-link
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Uninstall
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pi uninstall npm:pi-link
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Usage
|
|
59
|
+
|
|
60
|
+
Link is **off by default**. Start Pi with `--link` to auto-connect on startup:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Terminal 1 Terminal 2
|
|
64
|
+
---------- ----------
|
|
65
|
+
$ pi --link $ pi --link
|
|
66
|
+
✓ Link hub started on :9900 as "t-a1b2" ✓ Joined link as "t-c3d4" (2 online)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Use `pi-link <name>` to connect with a meaningful name and session resume:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
$ pi-link builder $ pi-link reviewer
|
|
73
|
+
✓ Link hub started on :9900 as "builder" ✓ Joined link as "reviewer" (2 online)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
See [Session Resume](#session-resume) for details.
|
|
77
|
+
|
|
78
|
+
Already in a session? Connect mid-session with `/link-connect`.
|
|
79
|
+
|
|
80
|
+
Use `/link` in any terminal to check status, or let the LLM tools handle cross-terminal coordination.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Walkthrough
|
|
85
|
+
|
|
86
|
+
Here's a concrete example of two terminals collaborating. Open two separate `pi --link` sessions.
|
|
87
|
+
|
|
88
|
+
**Terminal 1** - rename and check status:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
> /link-name builder
|
|
92
|
+
✓ Renamed to "builder"
|
|
93
|
+
|
|
94
|
+
> /link
|
|
95
|
+
⚡ Link: builder (hub) · 2 online
|
|
96
|
+
builder: idle (5s)
|
|
97
|
+
cwd: ~/my-project
|
|
98
|
+
researcher: idle (12s)
|
|
99
|
+
cwd: ~/my-project
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Terminal 2** - rename it too:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
> /link-name researcher
|
|
106
|
+
✓ Reconnecting as "researcher" (hub may assign a different name if taken)...
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Now ask Terminal 1's LLM to delegate work:**
|
|
110
|
+
|
|
111
|
+
In Terminal 1, type a normal prompt:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
> Use link_prompt to ask "researcher" to summarize the contents of README.md in this directory
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The LLM in Terminal 1 calls `link_prompt` → Terminal 2's LLM receives the prompt, reads the file, and sends back a summary → Terminal 1's LLM presents the result to you.
|
|
118
|
+
|
|
119
|
+
**Or broadcast a message to all terminals:**
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
> /link-broadcast starting the deployment pipeline
|
|
123
|
+
✓ Broadcast sent
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Every other terminal sees:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
⚡ [builder] starting the deployment pipeline
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
Link is **off by default**. Without `--link` or `pi-link`, the extension is completely silent — no status bar, no connections, no warnings.
|
|
137
|
+
|
|
138
|
+
| Method | When | Auto-reconnect? |
|
|
139
|
+
| ------------------ | ----------------------------------- | -------------------------------- |
|
|
140
|
+
| `pi-link <name>` | Resume/create named session | Yes |
|
|
141
|
+
| `pi --link` | Connect on startup (random name) | Yes |
|
|
142
|
+
| `/link-connect` | Opt-in mid-session (no flag needed) | Yes |
|
|
143
|
+
| `/link-disconnect` | Opt-out mid-session | Suppressed until `/link-connect` |
|
|
144
|
+
|
|
145
|
+
**Name precedence:** `PI_LINK_NAME` env (set by `pi-link`) > saved `/link-name` > Pi session name > random `t-xxxx`.
|
|
146
|
+
|
|
147
|
+
`/link-connect` and `/link-disconnect` save their intent to the session — resume later and the connection state is restored without needing the flag. Explicit user intent takes precedence over `--link`.
|
|
148
|
+
|
|
149
|
+
Once connected, terminals discover each other on `127.0.0.1:9900`. See [Limitations](#limitations--design-decisions) for the hardcoded port.
|
|
150
|
+
|
|
151
|
+
### Session Resume
|
|
152
|
+
|
|
153
|
+
Pi's `--session` flag requires a file path, not a display name. `pi-link` bridges this — it resolves a session by name and launches Pi directly:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
pi-link worker-1 # resume or create session "worker-1"
|
|
157
|
+
pi-link worker-1 --model sonnet # with extra Pi flags
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
How it works: `pi-link worker-1` scans `~/.pi/agent/sessions/`, finds the session named "worker-1", and launches `pi --session <path> --link`.
|
|
161
|
+
|
|
162
|
+
- **One match** → resumes that session
|
|
163
|
+
- **No match** → creates a new session
|
|
164
|
+
- **Multiple matches** → prints candidates to stderr, exits 1
|
|
165
|
+
|
|
166
|
+
`pi-link resolve <name>` is also available for machine-readable output (prints just the session path).
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## LLM Tools
|
|
171
|
+
|
|
172
|
+
The extension registers three tools that the LLM can invoke during agent runs. pi-link also ships with a bundled **pi-link-coordination** skill that gives agents on-demand guidance for tool selection, delegation patterns, and avoiding common coordination mistakes.
|
|
173
|
+
|
|
174
|
+
### Which tool should I use?
|
|
175
|
+
|
|
176
|
+
| Tool | Behavior | Returns |
|
|
177
|
+
| ------------- | ---------------------------------------------------- | ----------------------------------------- |
|
|
178
|
+
| `link_send` | Send a message; optionally trigger the remote LLM | Send/delivery status only |
|
|
179
|
+
| `link_prompt` | Run a prompt on a remote terminal and wait for reply | The remote terminal's assistant response |
|
|
180
|
+
| `link_list` | List currently connected terminals | Terminal list with roles, status, and cwd |
|
|
181
|
+
|
|
182
|
+
**If you need the other terminal's answer back, use `link_prompt`.** Use `link_send` to notify or steer without waiting.
|
|
183
|
+
|
|
184
|
+
### `link_send`
|
|
185
|
+
|
|
186
|
+
Send a fire-and-forget chat message to a specific terminal or broadcast to all.
|
|
187
|
+
|
|
188
|
+
| Parameter | Type | Description |
|
|
189
|
+
| ------------- | --------- | ---------------------------------------------------- |
|
|
190
|
+
| `to` | `string` | Target terminal name, or `"*"` for broadcast |
|
|
191
|
+
| `message` | `string` | Message content |
|
|
192
|
+
| `triggerTurn` | `boolean` | If `true`, the receiver's LLM responds automatically |
|
|
193
|
+
|
|
194
|
+
When `triggerTurn` is enabled, the message is queued in the receiver's local inbox. Nearby arrivals are coalesced (200ms debounce), and delivery is gated on the receiving agent being idle - ensuring it starts a clean new turn. Messages arrive as a single `[Link: N message(s) received]` block at the top of a fresh turn, not mid-run. When `triggerTurn` is `false` or omitted, delivery is immediate fire-and-forget.
|
|
195
|
+
|
|
196
|
+
Note: `triggerTurn` does **not** cause the response to come back to the caller - use `link_prompt` for that.
|
|
197
|
+
|
|
198
|
+
> **Broadcast note:** Sending to `"*"` delivers to **all other terminals** - the sender is excluded.
|
|
199
|
+
|
|
200
|
+
Pre-validates the target name against the local terminal list before sending, catching typos early. See [Message Routing](#message-routing--error-handling) for delivery semantics.
|
|
201
|
+
|
|
202
|
+
### `link_prompt`
|
|
203
|
+
|
|
204
|
+
Send a prompt to a remote terminal and **wait** for the LLM's response (synchronous RPC pattern).
|
|
205
|
+
|
|
206
|
+
| Parameter | Type | Description |
|
|
207
|
+
| --------- | -------- | -------------------- |
|
|
208
|
+
| `to` | `string` | Target terminal name |
|
|
209
|
+
| `prompt` | `string` | Prompt text to send |
|
|
210
|
+
|
|
211
|
+
- The remote terminal processes the prompt via `pi.sendUserMessage()` - as if a user typed it.
|
|
212
|
+
- Returns the remote terminal's actual assistant reply text as the tool result.
|
|
213
|
+
- **Self-target rejection** - prompting yourself (`to` equals your own name) returns an immediate error.
|
|
214
|
+
- **Heartbeat-based timeout** - no short fixed deadline. The target sends keepalives every 30s while working. The sender resets a 90-second inactivity timer on each keepalive. A 30-minute hard ceiling acts as a safety net against broken-but-chatty targets. A 10-minute task with regular activity never times out; a genuinely dead target times out in 90 seconds of silence.
|
|
215
|
+
- **Immediate failure on disconnect** - if the target leaves the network (`terminal_left`), pending prompts to that target fail immediately instead of waiting for the inactivity timeout.
|
|
216
|
+
- **Early failure detection** - if the message can't be delivered (e.g., target not found), the tool resolves immediately with an error instead of waiting for the timeout.
|
|
217
|
+
- Supports abort signals.
|
|
218
|
+
- Targets **one terminal at a time** (no broadcast mode).
|
|
219
|
+
- Only **one remote prompt** can execute at a time per target terminal. Concurrent requests are rejected with `"Terminal is busy"`.
|
|
220
|
+
|
|
221
|
+
### `link_list`
|
|
222
|
+
|
|
223
|
+
Lists all connected terminals with role info, live agent status, working directory, and self-identification. Takes no parameters.
|
|
224
|
+
|
|
225
|
+
Each terminal reports its current working directory on connect. `link_list` shows the full absolute path so agents can choose the right target, use explicit paths when terminals differ, and catch wrong-project mistakes early.
|
|
226
|
+
|
|
227
|
+
Each terminal's status is derived automatically from Pi lifecycle events - agents can't set it manually. Three states:
|
|
228
|
+
|
|
229
|
+
| Status | Meaning |
|
|
230
|
+
| ----------------- | ----------------------- |
|
|
231
|
+
| `idle (2m)` | Waiting for user input |
|
|
232
|
+
| `thinking (3s)` | LLM is generating |
|
|
233
|
+
| `tool:bash (12s)` | Running a specific tool |
|
|
234
|
+
|
|
235
|
+
Durations are computed at render time from a `since` timestamp - no timer traffic over the wire. Terminals that just joined with no status data yet render as blank, not fake idle.
|
|
236
|
+
|
|
237
|
+
Working directories use full absolute paths in tool output. In the TUI (`/link`), paths are shortened to `~/...` when possible to keep the display compact.
|
|
238
|
+
|
|
239
|
+
**Example output:**
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
Connected terminals:
|
|
243
|
+
• opus@pi-link (you) idle (12s)
|
|
244
|
+
cwd: C:\Users\andre\.pi
|
|
245
|
+
• gpt@pi-link thinking (3s)
|
|
246
|
+
cwd: C:\Users\andre\.pi
|
|
247
|
+
• docs@pi-link idle (1m)
|
|
248
|
+
cwd: C:\Users\andre\.pi
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Slash Commands
|
|
254
|
+
|
|
255
|
+
| Command | Purpose |
|
|
256
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
257
|
+
| `/link` | Show link status (name, role, online count, agent status, and cwd per terminal) |
|
|
258
|
+
| `/link-name [name]` | Rename and save as this session's preferred link name. With no argument, adopts the Pi session name. Restored on resume. |
|
|
259
|
+
| `/link-broadcast <msg>` | Broadcast a chat message to all other terminals |
|
|
260
|
+
| `/link-connect` | Connect to Pi Link (works anytime, with or without `--link`) |
|
|
261
|
+
| `/link-disconnect` | Disconnect from Pi Link and suppress auto-reconnect (overrides `--link`) |
|
|
262
|
+
|
|
263
|
+
### Examples
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
> /link
|
|
267
|
+
⚡ Link: builder (hub) · 3 online
|
|
268
|
+
builder: idle (12s)
|
|
269
|
+
cwd: ~/my-project
|
|
270
|
+
worker-1: thinking (3s)
|
|
271
|
+
cwd: ~/my-project
|
|
272
|
+
worker-2: tool:bash (5s)
|
|
273
|
+
cwd: ~/other-project
|
|
274
|
+
|
|
275
|
+
> /link-name orchestrator
|
|
276
|
+
✓ Renamed to "orchestrator"
|
|
277
|
+
|
|
278
|
+
> /link-name
|
|
279
|
+
✓ Renamed to "my-session"
|
|
280
|
+
|
|
281
|
+
> /link-broadcast starting the build pipeline
|
|
282
|
+
✓ Broadcast sent
|
|
283
|
+
|
|
284
|
+
> /link-disconnect
|
|
285
|
+
✓ Disconnected from link
|
|
286
|
+
|
|
287
|
+
> /link-connect
|
|
288
|
+
✓ Joined link as "orchestrator" (3 online)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
With no argument, `/link-name` adopts the Pi session name. `/link-connect` joins an existing hub if one is running; otherwise it starts the hub.
|
|
292
|
+
|
|
293
|
+
**Name persistence:** `/link-name` saves your preferred name to the session. Resume later and it's restored automatically. If the name is taken, the hub assigns a variant (e.g., `"builder-2"`), but your preferred name stays saved for the next reconnect. See [Name Uniqueness & Persistence](#name-uniqueness--persistence) for details.
|
|
294
|
+
|
|
295
|
+
See [Configuration](#configuration) for details on `--link`, `/link-connect`, and `/link-disconnect` behavior.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Architecture
|
|
300
|
+
|
|
301
|
+
### Hub-Spoke Topology
|
|
302
|
+
|
|
303
|
+
The network topology is **hub-spoke (star)**:
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
+-----------+
|
|
307
|
+
| Hub |
|
|
308
|
+
| :9900 |
|
|
309
|
+
+-----+-----+
|
|
310
|
+
|
|
|
311
|
+
+--------------+--------------+
|
|
312
|
+
| | |
|
|
313
|
+
+---+---+ +---+---+ +---+---+
|
|
314
|
+
| pi-2 | | pi-3 | | pi-4 |
|
|
315
|
+
|client | |client | |client |
|
|
316
|
+
+-------+ +-------+ +-------+
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
- The **first terminal** to start becomes the **hub** - it runs a `WebSocketServer` on `127.0.0.1:9900`.
|
|
320
|
+
- **Subsequent terminals** connect as **clients** via plain WebSocket.
|
|
321
|
+
- All messages route **through the hub**; clients never talk directly to each other.
|
|
322
|
+
|
|
323
|
+
### Auto-Discovery Protocol
|
|
324
|
+
|
|
325
|
+
The discovery sequence runs on startup (with `--link` or `pi-link`) or when `/link-connect` is used. See [Configuration](#configuration) for details.
|
|
326
|
+
|
|
327
|
+
The sequence is a simple fallback:
|
|
328
|
+
|
|
329
|
+
1. Attempt to connect as a **client** to `127.0.0.1:9900`.
|
|
330
|
+
2. If connection fails → become the **hub** (start a WebSocket server on that port).
|
|
331
|
+
3. If both fail (rare race condition) → retry after a randomized 2-5 second backoff.
|
|
332
|
+
|
|
333
|
+
### Hub Promotion
|
|
334
|
+
|
|
335
|
+
When the hub disconnects, clients detect the WebSocket close event, enter `"disconnected"` state, and call `scheduleReconnect()`. The **first terminal to retry** becomes the new hub via the same initialize-or-fallback flow.
|
|
336
|
+
|
|
337
|
+
There is **no explicit leader election** - promotion is race-based.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Troubleshooting
|
|
342
|
+
|
|
343
|
+
### Port 9900 is already in use
|
|
344
|
+
|
|
345
|
+
If another process occupies port 9900, the terminal can't become the hub. It will attempt to connect as a client instead (which also fails if there's no real hub), then retry after 2-5 seconds. Free the port or modify `DEFAULT_PORT` in `index.ts` - see [Limitations](#limitations--design-decisions).
|
|
346
|
+
|
|
347
|
+
### "Terminal is busy" rejections
|
|
348
|
+
|
|
349
|
+
Each terminal can only execute **one remote prompt at a time**. If a `link_prompt` arrives while the agent is already running (either from a local user or another remote prompt), it's immediately rejected with `"Terminal is busy"`. There is no queuing. Solutions:
|
|
350
|
+
|
|
351
|
+
- Wait for the target terminal to finish its current task.
|
|
352
|
+
- Spread prompts across multiple worker terminals.
|
|
353
|
+
- Have the sender retry after a delay.
|
|
354
|
+
|
|
355
|
+
### Terminals don't see each other
|
|
356
|
+
|
|
357
|
+
- Verify both terminals are on the same machine (the link only works on `127.0.0.1`).
|
|
358
|
+
- Run `/link` in each terminal to check status.
|
|
359
|
+
- Ensure port 9900 isn't blocked or occupied by a non-link process.
|
|
360
|
+
|
|
361
|
+
### Hub promotion loses state
|
|
362
|
+
|
|
363
|
+
When the hub goes down and a client promotes itself, terminal names and in-flight prompts from the old hub session are lost. All surviving clients reconnect and re-register. This is by design - see [Limitations](#limitations--design-decisions).
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Limitations & Design Decisions
|
|
368
|
+
|
|
369
|
+
| # | Decision | Rationale / Impact |
|
|
370
|
+
| --- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
371
|
+
| 1 | **No authentication** | Any localhost process can connect to port 9900. Acceptable for local dev; don't expose the port externally. |
|
|
372
|
+
| 2 | **Hardcoded port (9900)** | Not configurable without editing `DEFAULT_PORT` in `index.ts`. Could conflict with other services on the same port. |
|
|
373
|
+
| 3 | **Race-based hub promotion** | Non-deterministic. Terminal state (names, in-flight prompts) is lost during promotion. Simple but imperfect. |
|
|
374
|
+
| 4 | **Single remote prompt per terminal** | No queuing - immediate rejection if busy. See [`link_prompt`](#link_prompt) and [Troubleshooting](#terminal-is-busy-rejections). |
|
|
375
|
+
| 5 | **No message persistence** | Purely ephemeral WebSocket frames. Messages are lost if the recipient is offline. |
|
|
376
|
+
| 6 | **Client rename triggers full reconnect** | Changing a client's name requires a new `register` message, so the client disconnects and reconnects. Hub renames are handled in-place with collision checks. |
|
|
377
|
+
| 7 | **Single-machine / localhost-only** | Link only binds to `127.0.0.1`; terminals on different machines cannot join. |
|
|
378
|
+
| 8 | **Rename during prompt loses keepalives** | If the target renames mid-prompt, keepalive resets stop working (pending requests track by name). The final response can still succeed by request ID, but inactivity may false-fire on long tasks after rename. |
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Dependencies
|
|
383
|
+
|
|
384
|
+
### Runtime (installed by `pi install`)
|
|
385
|
+
|
|
386
|
+
| Package | Version | Purpose |
|
|
387
|
+
| ------- | ------- | ----------------------------------- |
|
|
388
|
+
| `ws` | ^8.20.0 | WebSocket library (server + client) |
|
|
389
|
+
|
|
390
|
+
### Development
|
|
391
|
+
|
|
392
|
+
| Package | Version | Purpose |
|
|
393
|
+
| ----------- | ------- | --------------------------- |
|
|
394
|
+
| `@types/ws` | ^8.18.1 | TypeScript type definitions |
|
|
395
|
+
|
|
396
|
+
### Provided by Pi (no install needed)
|
|
397
|
+
|
|
398
|
+
| Package | Purpose |
|
|
399
|
+
| ------------------------------- | ------------------------------------------------ |
|
|
400
|
+
| `@mariozechner/pi-coding-agent` | Pi SDK types (ExtensionAPI, ExtensionContext) |
|
|
401
|
+
| `@mariozechner/pi-tui` | TUI Text widget for custom message rendering |
|
|
402
|
+
| `@sinclair/typebox` | JSON Schema type definitions for tool parameters |
|
|
403
|
+
|
|
404
|
+
### `package.json`
|
|
405
|
+
|
|
406
|
+
```json
|
|
407
|
+
{
|
|
408
|
+
"name": "pi-link",
|
|
409
|
+
"bin": {
|
|
410
|
+
"pi-link": "./bin/pi-link.mjs"
|
|
411
|
+
},
|
|
412
|
+
"dependencies": {
|
|
413
|
+
"ws": "^8.20.0"
|
|
414
|
+
},
|
|
415
|
+
"devDependencies": {
|
|
416
|
+
"@types/ws": "^8.18.1"
|
|
417
|
+
},
|
|
418
|
+
"pi": {
|
|
419
|
+
"extensions": ["./index.ts"],
|
|
420
|
+
"skills": ["./skills"]
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
`pi.extensions` tells Pi which files to load as extensions. `pi.skills` registers bundled skill directories. `bin` exposes the `pi-link` CLI (see [Configuration](#configuration)).
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Internals
|
|
430
|
+
|
|
431
|
+
> This section covers implementation details for contributors and developers who want to understand or modify the extension's internals.
|
|
432
|
+
|
|
433
|
+
### Protocol
|
|
434
|
+
|
|
435
|
+
The wire protocol consists of **9 message types**, all serialized as JSON over WebSocket frames. Cwd-related fields are optional for backward compatibility.
|
|
436
|
+
|
|
437
|
+
| Type | Direction | Purpose |
|
|
438
|
+
| ----------------- | --------------- | ----------------------------------------------------------------------- |
|
|
439
|
+
| `register` | Client → Hub | First message after connecting; requests a name, optionally reports cwd |
|
|
440
|
+
| `welcome` | Hub → Client | Confirms assigned name, terminal list + status/cwd snapshots |
|
|
441
|
+
| `terminal_joined` | Hub → All | Broadcast when a terminal joins; may include cwd |
|
|
442
|
+
| `terminal_left` | Hub → All | Broadcast when a terminal disconnects |
|
|
443
|
+
| `chat` | Any → Any/All | Fire-and-forget message; optionally triggers LLM turn |
|
|
444
|
+
| `prompt_request` | Any → Any | Request a remote terminal to execute a prompt |
|
|
445
|
+
| `prompt_response` | Any → Any | Response carrying the remote prompt result |
|
|
446
|
+
| `status_update` | Any → Hub → All | Terminal broadcasts its agent status change |
|
|
447
|
+
| `error` | Hub → Client | Error notification |
|
|
448
|
+
|
|
449
|
+
### Message Flow Examples
|
|
450
|
+
|
|
451
|
+
**Joining the link:**
|
|
452
|
+
|
|
453
|
+
```
|
|
454
|
+
Client Hub
|
|
455
|
+
| |
|
|
456
|
+
| register {name:"builder", |
|
|
457
|
+
| cwd:"C:\\Users\\..."} |
|
|
458
|
+
|---------------------------->|
|
|
459
|
+
| |
|
|
460
|
+
| welcome {name, terminals, |
|
|
461
|
+
| statuses, cwds} |
|
|
462
|
+
|<----------------------------|
|
|
463
|
+
| |
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Hub then broadcasts `terminal_joined` to the other connected terminals. The `welcome` message includes status and cwd snapshots for all connected terminals (fields omitted above for brevity). `terminal_joined` also includes the new terminal's optional cwd.
|
|
467
|
+
|
|
468
|
+
**Sending a chat message:**
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
Client A Hub Client B
|
|
472
|
+
| | |
|
|
473
|
+
| chat {to:pi-2} | |
|
|
474
|
+
|----------------->| |
|
|
475
|
+
| | chat {from:A} |
|
|
476
|
+
| |----------------->|
|
|
477
|
+
| | |
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**Remote prompt (synchronous RPC):**
|
|
481
|
+
|
|
482
|
+
```
|
|
483
|
+
Client A Hub Client B
|
|
484
|
+
| | |
|
|
485
|
+
| prompt_request | |
|
|
486
|
+
|----------------->| |
|
|
487
|
+
| | prompt_request |
|
|
488
|
+
| |----------------->|
|
|
489
|
+
| | (LLM runs) |
|
|
490
|
+
| |<-----------------|
|
|
491
|
+
| prompt_response | |
|
|
492
|
+
|<-----------------| |
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Name Uniqueness & Persistence
|
|
496
|
+
|
|
497
|
+
The hub enforces unique terminal names via a `uniqueName()` function. If `"builder"` is already taken, the next terminal requesting that name is assigned `"builder-2"`, then `"builder-3"`, and so on.
|
|
498
|
+
|
|
499
|
+
Default names are random 4-character hex IDs: `t-a1b2`, `t-c3d4`, etc.
|
|
500
|
+
|
|
501
|
+
**Persistence:** `/link-name` saves the preferred name to the session via `pi.appendEntry("link-name", { name })`. On session resume, the saved name is restored and requested from the hub. Only explicit `/link-name` calls persist - hub-assigned variants like `"builder-2"` are not saved. On reconnect, the terminal always requests the preferred name, not the last runtime name.
|
|
502
|
+
|
|
503
|
+
**Rename guards:**
|
|
504
|
+
|
|
505
|
+
- If you're already using the requested name, `/link-name` returns early (`"Already using..."`).
|
|
506
|
+
- On the hub, renaming checks if the name is taken by another connected client before accepting the change.
|
|
507
|
+
- On a client, the rename triggers a reconnect; the hub enforces uniqueness during re-registration and may assign a different name if taken.
|
|
508
|
+
|
|
509
|
+
**Unregistered client guard:** The hub ignores all non-`register` messages from clients that haven't completed registration, preventing protocol violations from malformed or out-of-order messages.
|
|
510
|
+
|
|
511
|
+
### State Management
|
|
512
|
+
|
|
513
|
+
| State Field | Type | Purpose |
|
|
514
|
+
| ------------------------ | ------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
515
|
+
| `role` | `"hub" \| "client" \| "disconnected"` | Current network role |
|
|
516
|
+
| `agentRunning` | `boolean` | Whether an agent run is active; blocks incoming remote prompts |
|
|
517
|
+
| `activeToolName` | `string \| null` | Name of the currently executing tool (drives `tool:<name>` status) |
|
|
518
|
+
| `stateSince` | `number` | Timestamp of last status change (used for duration display) |
|
|
519
|
+
| `currentCwd` | `string` | Current working directory reported to peers on connect |
|
|
520
|
+
| `inbox` | `array` | Queued `triggerTurn:true` messages awaiting idle-gated flush |
|
|
521
|
+
| `flushTimer` | `Timer \| null` | Pending inbox flush (debounce or busy-retry) |
|
|
522
|
+
| `disposed` | `boolean` | Set on `session_shutdown`; guards all WebSocket callbacks against stale context |
|
|
523
|
+
| `startupConnectTimer` | `Timer \| null` | Deferred startup connect (`setTimeout(0)`) so Pi's startup cycle completes first |
|
|
524
|
+
| `manuallyDisconnected` | `boolean` | Set by `/link-disconnect`; suppresses auto-reconnect |
|
|
525
|
+
| `pendingRemotePrompt` | `object \| null` | Tracks the single in-flight remote prompt execution |
|
|
526
|
+
| `pendingPromptResponses` | `Map` | Outstanding prompt RPCs awaiting responses (includes inactivity + ceiling timers per entry) |
|
|
527
|
+
|
|
528
|
+
### Message Routing & Error Handling
|
|
529
|
+
|
|
530
|
+
`routeMessage()` returns a `boolean` indicating delivery status:
|
|
531
|
+
|
|
532
|
+
- **Hub** - delivery is authoritative. If the target terminal isn't connected, the hub sends a protocol-level error back to the sender. For `prompt_request` messages to unknown targets, the hub sends a `prompt_response` with an error field so the sender's pending promise resolves immediately rather than timing out.
|
|
533
|
+
- **Client** - delivery is optimistic (`true` means "sent to hub"). The hub handles routing and errors via the protocol.
|
|
534
|
+
|
|
535
|
+
### Connection Lifecycle
|
|
536
|
+
|
|
537
|
+
Internally, teardown is split into two functions:
|
|
538
|
+
|
|
539
|
+
- **`disconnect()`** - closes sockets, clears connection state, resolves pending promises. Used by `/link-disconnect` and called internally by `cleanup()`.
|
|
540
|
+
- **`cleanup()`** - calls `disconnect()`, sets `disposed = true`, clears `ctx`. Used on `session_shutdown`.
|
|
541
|
+
|
|
542
|
+
Three helpers protect WebSocket callbacks from stale extension context:
|
|
543
|
+
|
|
544
|
+
- **`getUi()`** - safely accesses `ctx.ui`, returns `null` if the context is invalidated.
|
|
545
|
+
- **`notify()`** - wraps `getUi()?.notify()` for safe notification delivery.
|
|
546
|
+
- **`isRuntimeLive()`** - returns `false` if `disposed` or context is stale; checked before processing any incoming WebSocket message.
|
|
547
|
+
|
|
548
|
+
Startup connect is deferred via `scheduleStartupConnect()` (`setTimeout(0)`) so Pi's startup cycle completes and the extension context is fully valid before WebSocket work begins.
|
|
549
|
+
|
|
550
|
+
The `manuallyDisconnected` flag distinguishes user-initiated disconnects (`/link-disconnect`) from connection loss. When set, `scheduleReconnect()` is suppressed - the terminal stays offline until `/link-connect` is explicitly called.
|
|
551
|
+
|
|
552
|
+
### Agent Lifecycle Integration
|
|
553
|
+
|
|
554
|
+
The extension hooks into Pi's agent lifecycle events:
|
|
555
|
+
|
|
556
|
+
- **`agent_start`** → Sets `agentRunning = true`, blocking incoming remote prompts. Broadcasts `status_update` (`thinking`).
|
|
557
|
+
- **`agent_end`** → Wakes up the inbox flush (idle-gated delivery for `triggerTurn:true` messages). Checks if a remote prompt was running; if so, extracts the last assistant response from `event.messages` and sends back a `prompt_response`. Broadcasts `status_update` (`idle`).
|
|
558
|
+
- **`tool_execution_start`** → Broadcasts `status_update` (`tool:<name>`).
|
|
559
|
+
- **`tool_execution_end`** → Clears tool status; broadcasts `status_update` (`thinking`) while the agent run continues.
|
|
560
|
+
- **`session_shutdown`** → Full cleanup via `cleanup()`: closes all sockets, resolves pending promises, and disposes the extension.
|
|
561
|
+
|
|
562
|
+
Status updates are push-based: each terminal broadcasts changes to the hub, which fans them out. New joiners receive a status snapshot for all terminals in the `welcome` message.
|
|
563
|
+
|
|
564
|
+
While executing a remote prompt, the target sends a forced `status_update` every 30 seconds as a keepalive - reusing the existing status push mechanism. On the sender side, each incoming `status_update` from the target resets the 90-second inactivity timer. All resolution paths (response, inactivity, ceiling, abort, disconnect, delivery failure) go through a single `cleanupPending()` helper to prevent double-resolution races.
|
|
565
|
+
|
|
566
|
+
### Idle-Gated Inbox
|
|
567
|
+
|
|
568
|
+
When a `chat` message arrives with `triggerTurn:true`, it goes into a local inbox instead of calling `pi.sendMessage()` immediately. This avoids a Pi platform race where steering messages sent mid-agent-run can be stranded (see `REPORT-sendMessage-race.md`).
|
|
569
|
+
|
|
570
|
+
The flush pipeline:
|
|
571
|
+
|
|
572
|
+
1. **Debounce** - `scheduleFlush(FLUSH_DELAY_MS)` coalesces burst arrivals (200ms window).
|
|
573
|
+
2. **Idle gate** - `flushInbox()` checks `ctx.isIdle()`. If busy, retries every 500ms.
|
|
574
|
+
3. **Batch** - up to 20 messages or ~16 000 chars per delivery (soft cap - the first item is always included even if oversized).
|
|
575
|
+
4. **Deliver** - one `pi.sendMessage({ triggerTurn: true })` call with a `[Link: N message(s) received]` block.
|
|
576
|
+
5. **Drain** - if the inbox still has items, reschedule.
|
|
577
|
+
|
|
578
|
+
On `agent_end`, the inbox flush is kicked via `scheduleFlush(0)` - deferred to the next macrotask, by which time `ctx.isIdle()` returns `true`.
|
|
579
|
+
|
|
580
|
+
| Constant | Value | Purpose |
|
|
581
|
+
| ----------------- | ------ | ---------------------------------------- |
|
|
582
|
+
| `FLUSH_DELAY_MS` | 200 | Burst debounce window |
|
|
583
|
+
| `IDLE_RETRY_MS` | 500 | Busy-retry polling interval |
|
|
584
|
+
| `BATCH_MAX_ITEMS` | 20 | Max messages per batch |
|
|
585
|
+
| `BATCH_MAX_CHARS` | 16 000 | Soft cap on batch text size (~4K tokens) |
|
|
586
|
+
|
|
587
|
+
### Rendering
|
|
588
|
+
|
|
589
|
+
Incoming link chat messages render with a styled `⚡ [sender]` prefix using the theme's accent color. The link status text in Pi's footer uses `theme.fg("dim", ...)` to match Pi's standard footer styling.
|