@agfpd/iapeer 0.2.78 → 0.2.79

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/LICENSE CHANGED
@@ -187,7 +187,7 @@
187
187
  same "printed page" as the copyright notice for easier
188
188
  identification within third-party archives.
189
189
 
190
- Copyright [yyyy] [name of copyright owner]
190
+ Copyright 2026 Artur Agafapudov
191
191
 
192
192
  Licensed under the Apache License, Version 2.0 (the "License");
193
193
  you may not use this file except in compliance with the License.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.2.78",
3
+ "version": "0.2.79",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
package/src/cli/index.ts CHANGED
@@ -925,7 +925,7 @@ const USAGE = `usage: iapeer <verb> [args]
925
925
  it up on the peer's first message); always-on: launchctl bootstrap. --all = every registered peer
926
926
  remove <peer> [--force] delete a peer's registry record (locked writer); refuses a LIVE peer unless --force
927
927
  send <target> (--message <text> | --message-file <f|->) [--from <id>] [--attachment <p>]… [--topic <t>] manual IAP send (fallback)
928
- <runtime> launch the cwd's peer (ALWAYS fresh)
928
+ <runtime> launch the cwd's peer FRESH; on a TTY drop into it (like attach), else detached
929
929
  connect telegram <peer> [--token <t>] attach a telegram bot to a peer (bot add → interface → router restart; asks only the token)
930
930
  enable <plugin> [peer] [--no-setup] install + enable an agfpd capability for a peer
931
931
  new <peer> [runtime] UNCONDITIONAL fresh restart: canary-clean kill + fresh wake (emergency lever for a hung/dead session — bypasses the peer; exit 0 = fresh session up+ready)
@@ -1735,36 +1735,8 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
1735
1735
  return 1
1736
1736
  }
1737
1737
  out(`${r.woke ? 'woke + ' : ''}attaching ${r.identity}…\n`)
1738
- // Attach-window observability (whether a death happened under attach was
1739
- // answerable only INDIRECTLY via wake cause=attach — i.e. only when the
1740
- // session was dead at attach time). Bracket the operator's client
1741
- // window in lifecycle.log: ev=attach at entry, ev=attach-end (with the tmux
1742
- // client's exit code) when the operator detaches or the server dies under
1743
- // the client. A death timestamp inside an attach…attach-end window now
1744
- // reads directly off the log.
1745
1738
  const attachCfg = loadLifecycleConfig(env)
1746
- appendLifecycleEvent(attachCfg.eventLogDir, { ev: 'attach', identity: r.identity, woke: String(r.woke) }, { env })
1747
- // SPAWN-FLIP Ф0b-3: a supervisor-HOSTED session has no tmux to attach — hand the operator to
1748
- // the supervisor client (runSupervisorClient: raw passthrough + repaint-on-attach + Ctrl-]
1749
- // detach + both reattach port-deps). It calls process.exit on every terminal path, so bracket
1750
- // attach-end via an exit hook (appendFileSync is signal/exit-safe). Flag-off → tmux attach.
1751
- if (hostSessionAlive(r.identity)) {
1752
- process.on('exit', () =>
1753
- appendLifecycleEvent(attachCfg.eventLogDir, { ev: 'attach-end', identity: r.identity, rc: process.exitCode ?? 0 }, { env }),
1754
- )
1755
- await runSupervisorClient(hostRunDir(), r.identity)
1756
- return 0 // unreachable — runSupervisorClient exits on detach / session end
1757
- }
1758
- // Drop the operator into the session. `env -u TMUX` so a nested attach from
1759
- // inside tmux does not error ("sessions should be nested with care").
1760
- const attachEnv = { ...env }
1761
- delete attachEnv.TMUX
1762
- const a = spawnSync('tmux', ['-S', r.socketPath, 'attach', '-t', r.identity], {
1763
- stdio: 'inherit',
1764
- env: attachEnv as Record<string, string>,
1765
- })
1766
- appendLifecycleEvent(attachCfg.eventLogDir, { ev: 'attach-end', identity: r.identity, rc: a.status ?? -1 }, { env })
1767
- return a.status ?? 0
1739
+ return await attachIntoSession(r.identity, r.socketPath, env, attachCfg.eventLogDir, r.woke)
1768
1740
  }
1769
1741
  case 'shadow': {
1770
1742
  // Read-only fidelity OBSERVER for the tmux→pty migration burn-in (track «b»). Compares
@@ -1823,14 +1795,25 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
1823
1795
  return await runSupervisorCli(rest)
1824
1796
  }
1825
1797
  default: {
1826
- // `iapeer <runtime>` (launch) — folder-launch the cwd's peer, ALWAYS fresh.
1798
+ // `iapeer <runtime>` (launch) — folder-launch the cwd's peer, ALWAYS fresh. On a TTY this is
1799
+ // the human "start a fresh session and work in it" verb (parallel to `attach`, which resumes):
1800
+ // after a successful fresh bring-up it drops the operator straight into the new session.
1801
+ // Non-TTY (scripted / piped) keeps the fire-and-forget behavior — report the launch and exit.
1827
1802
  if (verb && isRuntime(verb)) {
1828
- const r = await folderLaunch({ cwd: process.cwd(), runtime: verb, env })
1803
+ const cfg = loadLifecycleConfig(env)
1804
+ const id = resolveIdentity({ env })
1805
+ const r = await folderLaunch({ cwd: process.cwd(), runtime: verb, env, cfg })
1829
1806
  if (r.status === 'FAILED') {
1830
1807
  errOut(`launch: ${r.reason}\n`)
1831
1808
  return 1
1832
1809
  }
1833
- out(`launched ${r.process_address} (fresh)\n`)
1810
+ const identity = r.process_address ?? buildProcessAddress(verb, id.personality)
1811
+ if (process.stdin.isTTY && process.stdout.isTTY) {
1812
+ const socketPath = buildSocketPath(verb, id.personality, cfg.sockDir)
1813
+ out(`launched ${identity} (fresh) — attaching…\n`)
1814
+ return await attachIntoSession(identity, socketPath, env, cfg.eventLogDir, true)
1815
+ }
1816
+ out(`launched ${identity} (fresh)\n`)
1834
1817
  return 0
1835
1818
  }
1836
1819
  return usage(errOut)
@@ -1853,6 +1836,41 @@ function defaultFromIdentity(env: NodeJS.ProcessEnv): string {
1853
1836
  return resolveIdentity({ env }).address
1854
1837
  }
1855
1838
 
1839
+ /** Drop the operator into a live peer session — the interactive open shared by `attach` (resume)
1840
+ * and a TTY `iapeer <runtime>` (fresh launch). A supervisor-HOSTED session has no tmux to attach,
1841
+ * so the operator is handed to the supervisor client (raw passthrough + repaint-on-attach + Ctrl-]
1842
+ * detach); otherwise `tmux attach`. The operator window is bracketed in lifecycle.log
1843
+ * (ev=attach … ev=attach-end) so a death timestamp inside the window reads directly off the log.
1844
+ * Returns the client exit code. */
1845
+ async function attachIntoSession(
1846
+ identity: string,
1847
+ socketPath: string,
1848
+ env: NodeJS.ProcessEnv,
1849
+ eventLogDir: string,
1850
+ woke: boolean,
1851
+ ): Promise<number> {
1852
+ appendLifecycleEvent(eventLogDir, { ev: 'attach', identity, woke: String(woke) }, { env })
1853
+ if (hostSessionAlive(identity)) {
1854
+ // runSupervisorClient calls process.exit on every terminal path → bracket attach-end via an
1855
+ // exit hook (appendFileSync is signal/exit-safe).
1856
+ process.on('exit', () =>
1857
+ appendLifecycleEvent(eventLogDir, { ev: 'attach-end', identity, rc: process.exitCode ?? 0 }, { env }),
1858
+ )
1859
+ await runSupervisorClient(hostRunDir(), identity)
1860
+ return 0 // unreachable — runSupervisorClient exits on detach / session end
1861
+ }
1862
+ // `env -u TMUX` so a nested attach from inside tmux does not error ("sessions should be nested
1863
+ // with care").
1864
+ const attachEnv = { ...env }
1865
+ delete attachEnv.TMUX
1866
+ const a = spawnSync('tmux', ['-S', socketPath, 'attach', '-t', identity], {
1867
+ stdio: 'inherit',
1868
+ env: attachEnv as Record<string, string>,
1869
+ })
1870
+ appendLifecycleEvent(eventLogDir, { ev: 'attach-end', identity, rc: a.status ?? -1 }, { env })
1871
+ return a.status ?? 0
1872
+ }
1873
+
1856
1874
  if (import.meta.main) {
1857
1875
  runCli(process.argv.slice(2)).then(code => process.exit(code))
1858
1876
  }
@@ -1,12 +1,13 @@
1
1
  // Supervisor DAEMON — holds the pty + child + headless-xterm model + capability responder
2
- // independent of any terminal; clients attach/detach over a unix socket (cutover Block 2, ported
3
- // from the validated PoC pts.mjs). DARK: reachable only via the `supervisor` CLI verb (dynamic
4
- // import), so @xterm never loads in the delivery hot path. NOT wired into launch/lifecycle — it
5
- // serves NOTHING on the live fleet yet; it is validated on throwaway sessions only.
2
+ // independent of any terminal; clients attach/detach over a unix socket. It HOSTS tui/router peers
3
+ // by default (the pty-host launch path; opt-out per-peer via a `.no-pty-host` marker), and is also
4
+ // reachable manually via the `supervisor` CLI verb for throwaway validation. It runs in its OWN
5
+ // detached process, dynamic-imported from the launch path (supervisor/index.ts → daemon.ts), so
6
+ // @xterm never loads in the central router's delivery path nor in the launch/lifecycle process.
6
7
  //
7
8
  // Loads @xterm/headless + @xterm/addon-serialize (the latter is the repaint source: a reattaching
8
9
  // client gets the full current screen even if the child is silent). Both bundle under
9
- // `bun build --compile` (pure JS; the Bun-native pty has no native addon — PoC-proven).
10
+ // `bun build --compile` (pure JS; the Bun-native pty has no native addon).
10
11
  import { Terminal } from '@xterm/headless'
11
12
  import { SerializeAddon } from '@xterm/addon-serialize'
12
13
  import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, unlinkSync, writeFileSync, writeSync } from 'node:fs'