@agfpd/iapeer 0.2.20 → 0.2.21

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.20",
3
+ "version": "0.2.21",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -153,6 +153,37 @@ describe('send validation', () => {
153
153
  })
154
154
  })
155
155
 
156
+ describe('send → ephemeral target: M3 FIFO parity with the daemon path (iapeer-memory ask)', () => {
157
+ test('a CLI send to a wake_policy:ephemeral peer ENQUEUES (queued ack), no live/miss bypass', async () => {
158
+ // an ephemeral worker cwd (profile declares wake_policy) registered in the index
159
+ const cwd = mkdtempSync(join(tmpdir(), 'iapeer-cli-eph-'))
160
+ mkdirSync(join(cwd, '.iapeer'), { recursive: true })
161
+ writeFileSync(
162
+ join(cwd, '.iapeer', 'peer-profile.json'),
163
+ JSON.stringify({ personality: 'ephw', runtime: 'claude', runtimes: ['claude'], intelligence: 'artificial', wake_policy: 'ephemeral' }),
164
+ )
165
+ const e = env()
166
+ // routeSend resolves the peers index from the PROCESS env (transport reads
167
+ // readPeersIndex() bare) — point the process-level root at the sandbox too.
168
+ const prevRoot = process.env.IAPEER_ROOT
169
+ process.env.IAPEER_ROOT = root
170
+ try {
171
+ await upsertPeer({ personality: 'ephw', runtime: 'claude', cwd, intelligence: 'artificial' }, { rootDir: root })
172
+ await register('sender')
173
+ const r = await sendMessage({ from: 'claude-sender', target: 'ephw', message: 'task', env: e })
174
+ expect(r.queued).toBe(true) // serialized via the disk FIFO, exactly like the daemon path
175
+ expect(r.queueDepth).toBe(1)
176
+ // the task is durably on disk for the daemon tick to drain
177
+ const qdir = join(loadLifecycleConfig(e).stateDir, 'claude-ephw.queue')
178
+ expect(existsSync(qdir)).toBe(true)
179
+ } finally {
180
+ if (prevRoot !== undefined) process.env.IAPEER_ROOT = prevRoot
181
+ else delete process.env.IAPEER_ROOT
182
+ rmSync(cwd, { recursive: true, force: true })
183
+ }
184
+ })
185
+ })
186
+
156
187
  describe('--help/-h global intercept (CLI hygiene — usage printed, NOTHING executed)', () => {
157
188
  let captured: string
158
189
  let origWrite: typeof process.stdout.write
package/src/cli/index.ts CHANGED
@@ -323,9 +323,21 @@ export interface SendOptions extends CliEnvOptions {
323
323
  const cliWake: WakeFn = req =>
324
324
  wakeOrSpawn({ personality: req.personality, runtime: req.runtime, topic: req.topic, task: req.task })
325
325
 
326
- export async function sendMessage(opts: SendOptions): Promise<{ ok: true; delivered_to: { personality: string; runtime: string } }> {
326
+ export async function sendMessage(
327
+ opts: SendOptions,
328
+ ): Promise<{ ok: true; delivered_to: { personality: string; runtime: string }; queued?: boolean; queueDepth?: number }> {
327
329
  const env = opts.env ?? process.env
328
330
  const caller = resolveCallerIdentity(parseIdentity(opts.from), readPeersIndex({ env }))
331
+ // wake_policy:ephemeral M3 parity (iapeer-memory ask, 10.06): the CLI path used
332
+ // to route an ephemeral target through the normal live/miss path — a notifier
333
+ // burst landed as TURNS in one live worker session instead of serializing
334
+ // through the disk FIFO the daemon path uses. Same seam, ONE difference: the
335
+ // drain kick is a NOOP here — a CLI process exits right after the ack, so an
336
+ // unawaited in-process wake would die with it; the daemon's supervise-tick
337
+ // drain scan (≤60 s) picks the queue up — the EXISTING retry path for failed
338
+ // kicks, not a new mechanism.
339
+ const { makeEphemeralRouteDeps } = await import('../daemon/main.ts')
340
+ const cfg = loadLifecycleConfig(env)
329
341
  const result = await routeSend(
330
342
  caller,
331
343
  {
@@ -335,10 +347,15 @@ export async function sendMessage(opts: SendOptions): Promise<{ ok: true; delive
335
347
  topic: opts.topic,
336
348
  attachments: opts.attachments,
337
349
  },
338
- { wake: cliWake },
350
+ { wake: cliWake, ephemeral: makeEphemeralRouteDeps(cfg, env, () => {}) },
339
351
  )
340
352
  if (!result.ok) throw new Error(result.error.message)
341
- return { ok: true, delivered_to: result.value.delivered_to }
353
+ return {
354
+ ok: true,
355
+ delivered_to: result.value.delivered_to,
356
+ queued: result.value.queued,
357
+ queueDepth: result.value.queueDepth,
358
+ }
342
359
  }
343
360
 
344
361
  function parseIdentity(identity: string): { personality: string; runtime: Runtime } {
@@ -714,7 +731,11 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
714
731
  attachments: attachments.length ? attachments : undefined,
715
732
  env,
716
733
  })
717
- out(`delivered to ${r.delivered_to.personality} (${r.delivered_to.runtime})\n`)
734
+ out(
735
+ r.queued
736
+ ? `queued for ${r.delivered_to.personality} (${r.delivered_to.runtime}), depth ${r.queueDepth ?? '?'} — the daemon tick drains it\n`
737
+ : `delivered to ${r.delivered_to.personality} (${r.delivered_to.runtime})\n`,
738
+ )
718
739
  return 0
719
740
  }
720
741
  case 'version':
@@ -26,6 +26,18 @@ import { tmpdir } from 'os'
26
26
  import { join } from 'path'
27
27
  import { spawnSync } from 'child_process'
28
28
 
29
+ /** CROSS-PRODUCT CONTRACT (agreed with iapeer-memory, 10.06): this CN is the
30
+ * SHARED signing identity of the whole agfpd stack. Each product signs with its
31
+ * OWN --identifier (foundation: com.agfpd.iapeer; memory: com.agfpd.iapeer-memory),
32
+ * so TCC subjects stay separate while the host carries ONE key (one keychain
33
+ * prompt ever). Creation is first-needs-creates with the IDENTICAL profile (EKU
34
+ * codeSigning, system LibreSSL p12, import -T /usr/bin/codesign) on both sides.
35
+ * Changing the CN or the creation profile is a COORDINATED change across repos.
36
+ * Known shared costs: re-creating the identity (deleted/expired — cert is 10 y)
37
+ * migrates the TCC grants of EVERY stack product at once; a concurrent
38
+ * first-creation by two installers could duplicate the CN (codesign would then
39
+ * report an ambiguous identity) — installs are operator-sequential, residual
40
+ * risk accepted. */
29
41
  export const SIGNING_IDENTITY_CN = 'iapeer Local Codesign'
30
42
  export const SIGNING_IDENTIFIER = 'com.agfpd.iapeer'
31
43