@junghanacs/entwurf 0.41.0 → 0.41.2
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/AGENTS.md +1 -1
- package/BASELINE.md +7 -5
- package/CHANGELOG.md +426 -0
- package/DELIVERY.md +37 -20
- package/README.md +83 -81
- package/VERIFY.md +10 -10
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/external-mcp-host.md +1 -1
- package/docs/setup-clean-host.md +95 -29
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +14 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +10 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +23 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +3 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +21 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +4 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +5 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +6 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +13 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js +25 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +94 -28
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +57 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +2 -2
- package/mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js +12 -12
- package/mcp/entwurf-bridge/src/index.ts +14 -11
- package/native/win32-x64/entwurf_win.node +0 -0
- package/native/win32-x64/manifest.json +2 -2
- package/package.json +15 -14
- package/pi/pi-durable/carrier/tui.js +18 -3
- package/pi/pi-durable/overlay/upstream-pin.json +12 -12
- package/pi-extensions/lib/acp/backend-adapter.ts +5 -3
- package/pi-extensions/lib/acp/backend.ts +12 -9
- package/pi-extensions/lib/entwurf-self-address.ts +13 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +25 -2
- package/pi-extensions/lib/entwurf-v2-decider.ts +7 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +3 -0
- package/pi-extensions/lib/fresh-call-composition.ts +21 -10
- package/pi-extensions/lib/herdr-fresh-call.ts +4 -2
- package/pi-extensions/lib/meta-sender-identity.ts +4 -0
- package/pi-extensions/lib/mux-fresh-call.ts +6 -4
- package/pi-extensions/lib/native-push/register.ts +17 -2
- package/pi-extensions/lib/omp-fresh-preflight.ts +2 -2
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +1 -1
- package/pi-extensions/lib/remote-pair-receive.ts +27 -19
- package/pi-extensions/lib/remote-pair-send.ts +117 -30
- package/pi-extensions/meta-bridge-omp.ts +69 -7
- package/pi-extensions/meta-bridge-pi-durable.ts +3 -3
- package/run.sh +218 -49
- package/scripts/check-agy-sender-identity.ts +6 -4
- package/scripts/check-entwurf-self-address.ts +34 -3
- package/scripts/check-entwurf-v2-contract.ts +24 -2
- package/scripts/check-entwurf-v2-production.ts +45 -31
- package/scripts/check-gate-qualification.ts +11 -9
- package/scripts/check-native-push-adapter.ts +4 -0
- package/scripts/check-native-push-register.ts +32 -82
- package/scripts/check-remote-pair-installed.ts +2 -2
- package/scripts/dev-bin.sh +112 -1
- package/scripts/lib/chain-terminus-publication.ts +58 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +33 -2
- package/scripts/lib/codex-fresh-source-receipts.ts +112 -3
- package/scripts/lib/pi-durable-fresh-live-receipts.ts +29 -1
- package/scripts/mutants/chain-terminus-publication.json +17 -0
- package/scripts/mutants/codex-native.json +98 -0
- package/scripts/mutants/control-send-receipt.json +3 -3
- package/scripts/mutants/mux-fresh-call.json +16 -1
- package/scripts/mutants/native-push-paused.json +17 -0
- package/scripts/mutants/omp-fresh.json +13 -2
- package/scripts/mutants/pack-install.json +50 -4
- package/scripts/mutants/pi-durable.json +60 -0
- package/scripts/mutants/pi-floor.json +5 -5
- package/scripts/mutants/remote-pair-display.json +3 -3
- package/scripts/mutants/remote-pair-receive.json +41 -11
- package/scripts/mutants/remote-pair-roundtrip.json +67 -7
- package/scripts/mutants/remote-pair-send.json +156 -17
- package/scripts/mutants/remote-pair-transport.json +4 -4
- package/scripts/mutants/source-install.json +66 -0
- package/scripts/remote-pair-receive.ts +12 -12
- package/scripts/smoke-agy-install-state.sh +121 -0
- package/scripts/smoke-agy-native-push-live.ts +13 -0
- package/scripts/smoke-codex-fresh-live.ts +52 -28
- package/scripts/smoke-entwurf-chain-live.ts +7 -9
- package/scripts/smoke-herdr-fresh-call-live.ts +4 -4
- package/scripts/smoke-herdr-plugin-build-live.ts +179 -50
- package/scripts/smoke-herdr-raw-install-live.sh +10 -3
- package/scripts/smoke-mux-lifecycle-live.ts +140 -8
- package/scripts/smoke-omp-fresh-live.ts +5 -4
- package/scripts/smoke-pi-durable-fresh-live.ts +29 -7
- package/scripts/smoke-setup-verdict.sh +27 -2
|
@@ -430,9 +430,9 @@ ok(
|
|
|
430
430
|
// cannot enforce it — these semantic fixtures (the SSOT predicate the 5b decider
|
|
431
431
|
// mints against) do.
|
|
432
432
|
eq(
|
|
433
|
-
"?6: PRE_PROBE_REJECT_REASONS = bad-target/target-locked/target-address-conflict/record-less-socket",
|
|
433
|
+
"?6: PRE_PROBE_REJECT_REASONS = bad-target/target-locked/target-address-conflict/record-less-socket/native-push-paused",
|
|
434
434
|
[...PRE_PROBE_REJECT_REASONS],
|
|
435
|
-
["bad-target", "target-locked", "target-address-conflict", "record-less-socket"],
|
|
435
|
+
["bad-target", "target-locked", "target-address-conflict", "record-less-socket", "native-push-paused"],
|
|
436
436
|
);
|
|
437
437
|
// the partition is exhaustive + disjoint: every reject reason is pre-probe XOR not.
|
|
438
438
|
for (const r of ENTWURF_V2_REJECT_REASONS) {
|
|
@@ -467,6 +467,28 @@ ok(
|
|
|
467
467
|
"taxonomy: record-less-socket is NOT a resolver reason (target-resolution stage, not a table cell)",
|
|
468
468
|
!(RESOLVER_REJECT_REASONS as readonly string[]).includes("record-less-socket"),
|
|
469
469
|
);
|
|
470
|
+
|
|
471
|
+
// #152: native-push-paused — a native-push backend paused by policy (antigravity: its LS
|
|
472
|
+
// refused entwurf's measured external agentapi calls for a missing CSRF token). The decider refuses it BEFORE
|
|
473
|
+
// any adapter probe, so it is pre-probe (null liveness), never a resolver table cell and
|
|
474
|
+
// never one of the post-probe native-push outcomes it must not impersonate (dead /
|
|
475
|
+
// indeterminate).
|
|
476
|
+
ok(
|
|
477
|
+
"taxonomy: native-push-paused is in the enum (#152)",
|
|
478
|
+
(ENTWURF_V2_REJECT_REASONS as readonly string[]).includes("native-push-paused"),
|
|
479
|
+
);
|
|
480
|
+
ok(
|
|
481
|
+
"?6: native-push-paused is pre-probe (#152 — refused before any adapter probe)",
|
|
482
|
+
isPreProbeReject("native-push-paused"),
|
|
483
|
+
);
|
|
484
|
+
ok(
|
|
485
|
+
"taxonomy: native-push-paused is NOT a resolver reason (#152)",
|
|
486
|
+
!(RESOLVER_REJECT_REASONS as readonly string[]).includes("native-push-paused"),
|
|
487
|
+
);
|
|
488
|
+
ok(
|
|
489
|
+
"taxonomy: native-push-paused is NOT a post-probe native-push reject (#152 — not dead, not indeterminate)",
|
|
490
|
+
!(NATIVE_PUSH_REJECT_REASONS as readonly string[]).includes("native-push-paused"),
|
|
491
|
+
);
|
|
470
492
|
ok(
|
|
471
493
|
"taxonomy: the retired socket-only-no-resume-authority is GONE from the enum (#50 C4)",
|
|
472
494
|
!(ENTWURF_V2_REJECT_REASONS as readonly string[]).includes("socket-only-no-resume-authority"),
|
|
@@ -344,45 +344,54 @@ async function main(): Promise<void> {
|
|
|
344
344
|
ok("A2: a quarantined target is never lock-acquired", spies.acquire.length === 0);
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
// ── A3: native-push
|
|
347
|
+
// ── A3: antigravity native-push is PAUSED (#152) — the production factory refuses it ─
|
|
348
|
+
// The agy LS refused entwurf's measured external agentapi calls (missing CSRF token), so the public dispatch
|
|
349
|
+
// must stop at the decider with the named pre-probe reject: no adapter resolved, nothing
|
|
350
|
+
// probed or sent, no lock, no mailbox, and no sender envelope asked for. The probe fake is
|
|
351
|
+
// configured ALIVE on purpose — a regression that reached it would deliver, not hide.
|
|
348
352
|
{
|
|
349
353
|
const { deps, spies } = makeSpiedFactory({
|
|
350
354
|
backend: "antigravity",
|
|
351
355
|
nativePushProbe: { status: "alive", route: { backend: "antigravity", lsAddress: "127.0.0.1:5599" } },
|
|
352
356
|
});
|
|
353
357
|
const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "hi agy" }, deps);
|
|
354
|
-
ok("A3: antigravity ff → native-push delivered", result.kind === "executed" && result.transport === "native-push");
|
|
355
|
-
ok("A3: decider probed the native-push adapter once (decide side)", spies.nativePushProbe.length === 1);
|
|
356
|
-
ok("A3: executor sent via the native-push adapter once (execute side)", spies.nativePushSend.length === 1);
|
|
357
|
-
ok("A3: send used the DECIDER-probed route", spies.nativePushSend[0]?.route === "127.0.0.1:5599");
|
|
358
|
-
ok("A3: send carried the dispatch message", spies.nativePushSend[0]?.content.includes("hi agy") === true);
|
|
359
358
|
ok(
|
|
360
|
-
"A3:
|
|
361
|
-
|
|
359
|
+
"A3: antigravity ff → rejected native-push-paused (#152), observedLiveness null",
|
|
360
|
+
result.kind === "rejected" &&
|
|
361
|
+
result.receipt.reason === "native-push-paused" &&
|
|
362
|
+
result.receipt.observedLiveness === null,
|
|
362
363
|
);
|
|
363
|
-
ok("A3: native-push
|
|
364
|
-
ok(
|
|
364
|
+
ok("A3: paused agy resolved NO native-push adapter (neither hand)", spies.nativePushResolve.length === 0);
|
|
365
|
+
ok(
|
|
366
|
+
"A3: paused agy probed and sent nothing",
|
|
367
|
+
spies.nativePushProbe.length === 0 && spies.nativePushSend.length === 0,
|
|
368
|
+
);
|
|
369
|
+
ok("A3: paused agy took no lock and enqueued no mailbox", spies.acquire.length === 0 && spies.enqueue.length === 0);
|
|
370
|
+
ok("A3: paused agy never asked the sender provider (no execute phase)", spies.senderCalls.length === 0);
|
|
365
371
|
}
|
|
372
|
+
// ── A3c: native-push wiring — ONE injected adapter feeds BOTH decider + executor (Codex,
|
|
373
|
+
// the active native-push backend while agy is paused) ─
|
|
366
374
|
{
|
|
375
|
+
const socketPath = "/home/operator/.codex/app-server-control/app-server-control.sock";
|
|
367
376
|
const { deps, spies } = makeSpiedFactory({
|
|
368
377
|
backend: "codex",
|
|
369
|
-
nativePushProbe: {
|
|
370
|
-
status: "alive",
|
|
371
|
-
route: { backend: "codex", socketPath: "/home/operator/.codex/app-server-control/app-server-control.sock" },
|
|
372
|
-
},
|
|
378
|
+
nativePushProbe: { status: "alive", route: { backend: "codex", socketPath } },
|
|
373
379
|
});
|
|
374
380
|
const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "hi codex" }, deps);
|
|
375
381
|
ok(
|
|
376
382
|
"[QK:CODEX-NATIVE-PRODUCTION-WIRING] Codex ff → native-push delivered",
|
|
377
383
|
result.kind === "executed" && result.transport === "native-push",
|
|
378
384
|
);
|
|
385
|
+
ok("A3c: decider probed the native-push adapter once (decide side)", spies.nativePushProbe.length === 1);
|
|
386
|
+
ok("A3c: executor sent via the native-push adapter once (execute side)", spies.nativePushSend.length === 1);
|
|
379
387
|
ok(
|
|
380
|
-
"A3c:
|
|
381
|
-
spies.
|
|
388
|
+
"A3c: send used the DECIDER-probed route (the planted Codex UDS)",
|
|
389
|
+
spies.nativePushSend[0]?.route === socketPath,
|
|
382
390
|
);
|
|
391
|
+
ok("A3c: send carried the dispatch message", spies.nativePushSend[0]?.content.includes("hi codex") === true);
|
|
383
392
|
ok(
|
|
384
|
-
"A3c:
|
|
385
|
-
spies.
|
|
393
|
+
"A3c: BOTH hands resolved the SAME adapter (for 'codex')",
|
|
394
|
+
spies.nativePushResolve.length === 2 && spies.nativePushResolve.every((b) => b === "codex"),
|
|
386
395
|
);
|
|
387
396
|
ok(
|
|
388
397
|
"A3c: Codex native-push remains lock-free and mailbox-free",
|
|
@@ -394,16 +403,20 @@ async function main(): Promise<void> {
|
|
|
394
403
|
// Codex thread arrived as bare text, so the receiving citizen could not name who called it
|
|
395
404
|
// and the callback was uncorrelatable. Direct injection has neither the control socket's RPC
|
|
396
405
|
// framing nor a mailbox file, so the envelope must ride IN the body — the same bytes
|
|
397
|
-
// `formatMetaMailboxBody` gives the mailbox rail. ONE rail
|
|
398
|
-
//
|
|
406
|
+
// `formatMetaMailboxBody` gives the mailbox rail. ONE rail, no per-backend branch; it is
|
|
407
|
+
// proven on Codex, the native-push backend public dispatch still reaches while agy is
|
|
408
|
+
// paused (#152).
|
|
399
409
|
{
|
|
400
|
-
const route = {
|
|
410
|
+
const route = {
|
|
411
|
+
backend: "codex",
|
|
412
|
+
socketPath: "/home/operator/.codex/app-server-control/app-server-control.sock",
|
|
413
|
+
} as const;
|
|
401
414
|
const { deps, spies } = makeSpiedFactory({
|
|
402
|
-
backend: "
|
|
415
|
+
backend: "codex",
|
|
403
416
|
nativePushProbe: { status: "alive", route },
|
|
404
417
|
});
|
|
405
418
|
const { result, executeEvents } = await runInTwoPhases(
|
|
406
|
-
{ target: GID, intent: "fire-and-forget", message: "hi
|
|
419
|
+
{ target: GID, intent: "fire-and-forget", message: "hi codex", wantsReply: true },
|
|
407
420
|
deps,
|
|
408
421
|
spies,
|
|
409
422
|
);
|
|
@@ -417,12 +430,12 @@ async function main(): Promise<void> {
|
|
|
417
430
|
result.kind === "executed" && spies.senderCalls.length === 1,
|
|
418
431
|
);
|
|
419
432
|
ok(
|
|
420
|
-
"[QK:NATIVE-PUSH-SENDER-ENVELOPE]
|
|
421
|
-
spies.nativePushSend[0]?.content === formatMetaMailboxBody(SENDER, "hi
|
|
433
|
+
"[QK:NATIVE-PUSH-SENDER-ENVELOPE] native-push direct injection carries the mailbox-serialized trusted envelope",
|
|
434
|
+
spies.nativePushSend[0]?.content === formatMetaMailboxBody(SENDER, "hi codex", true),
|
|
422
435
|
);
|
|
423
436
|
ok(
|
|
424
437
|
"A3d: the enveloped body is NOT the raw plan message (an envelope was actually added)",
|
|
425
|
-
spies.nativePushSend[0]?.content !== "hi
|
|
438
|
+
spies.nativePushSend[0]?.content !== "hi codex",
|
|
426
439
|
);
|
|
427
440
|
ok(
|
|
428
441
|
"A3d: wants_reply rides the envelope on this rail",
|
|
@@ -439,7 +452,7 @@ async function main(): Promise<void> {
|
|
|
439
452
|
);
|
|
440
453
|
}
|
|
441
454
|
{
|
|
442
|
-
// Same rail,
|
|
455
|
+
// Same rail, wants_reply=false — the envelope is still rendered, without the reply line.
|
|
443
456
|
const route = {
|
|
444
457
|
backend: "codex",
|
|
445
458
|
socketPath: "/home/operator/.codex/app-server-control/app-server-control.sock",
|
|
@@ -447,7 +460,7 @@ async function main(): Promise<void> {
|
|
|
447
460
|
const { deps, spies } = makeSpiedFactory({ backend: "codex", nativePushProbe: { status: "alive", route } });
|
|
448
461
|
const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "hi codex" }, deps);
|
|
449
462
|
ok(
|
|
450
|
-
"A3d:
|
|
463
|
+
"A3d: wants_reply=false direct injection carries the mailbox-serialized envelope for that flag",
|
|
451
464
|
result.kind === "executed" &&
|
|
452
465
|
spies.nativePushSend[0]?.content === formatMetaMailboxBody(SENDER, "hi codex", false),
|
|
453
466
|
);
|
|
@@ -503,14 +516,15 @@ async function main(): Promise<void> {
|
|
|
503
516
|
}
|
|
504
517
|
|
|
505
518
|
{
|
|
506
|
-
//
|
|
519
|
+
// codex + dead probe → reject native-push-target-dead, NO send. (agy never reaches a
|
|
520
|
+
// probe while paused, #152 — A3.)
|
|
507
521
|
const { deps, spies } = makeSpiedFactory({
|
|
508
|
-
backend: "
|
|
522
|
+
backend: "codex",
|
|
509
523
|
nativePushProbe: { status: "dead", reason: "no host" },
|
|
510
524
|
});
|
|
511
525
|
const result = await runEntwurfV2({ target: GID, intent: "fire-and-forget", message: "x" }, deps);
|
|
512
526
|
ok(
|
|
513
|
-
"A3b:
|
|
527
|
+
"A3b: codex ff + dead → rejected native-push-target-dead",
|
|
514
528
|
result.kind === "rejected" && result.receipt.reason === "native-push-target-dead",
|
|
515
529
|
);
|
|
516
530
|
ok("A3b: no send attempted on a dead target", spies.nativePushSend.length === 0);
|
|
@@ -3459,7 +3459,9 @@ let manifestCount: number;
|
|
|
3459
3459
|
"retirement-event": 16,
|
|
3460
3460
|
"control-retire": 9,
|
|
3461
3461
|
"control-send-receipt": 19,
|
|
3462
|
-
"codex-native":
|
|
3462
|
+
"codex-native": 95,
|
|
3463
|
+
"native-push-paused": 1,
|
|
3464
|
+
"chain-terminus-publication": 1,
|
|
3463
3465
|
"compaction-send-guard": 7,
|
|
3464
3466
|
"control-socket-disconnect": 4,
|
|
3465
3467
|
"demo-reproducibility": 12,
|
|
@@ -3470,7 +3472,7 @@ let manifestCount: number;
|
|
|
3470
3472
|
"pi-bridge-sender": 4,
|
|
3471
3473
|
"pi-mcp-bridge": 2,
|
|
3472
3474
|
"pi-mcp-register": 3,
|
|
3473
|
-
"pi-durable":
|
|
3475
|
+
"pi-durable": 94,
|
|
3474
3476
|
"copilot-receive": 20,
|
|
3475
3477
|
"entwurf-peers": 1,
|
|
3476
3478
|
"fresh-call-dispatch": 15,
|
|
@@ -3491,30 +3493,30 @@ let manifestCount: number;
|
|
|
3491
3493
|
"meta-identity": 9,
|
|
3492
3494
|
"meta-retire": 4,
|
|
3493
3495
|
"mux-boundary": 16,
|
|
3494
|
-
"mux-fresh-call":
|
|
3496
|
+
"mux-fresh-call": 66,
|
|
3495
3497
|
"mux-launcher-fence": 8,
|
|
3496
3498
|
"mux-parent-artifact": 3,
|
|
3497
|
-
"pack-install":
|
|
3499
|
+
"pack-install": 9,
|
|
3498
3500
|
"pi-package-ownership": 8,
|
|
3499
3501
|
"qualification-ledger": 42,
|
|
3500
3502
|
"mux-resume-call": 13,
|
|
3501
3503
|
"omp-birth": 13,
|
|
3502
|
-
"omp-fresh":
|
|
3504
|
+
"omp-fresh": 25,
|
|
3503
3505
|
"omp-receive": 11,
|
|
3504
3506
|
"probe-ordering": 1,
|
|
3505
3507
|
"release-gate": 21,
|
|
3506
3508
|
"remote-pair-display": 3,
|
|
3507
3509
|
"remote-pair-installed-audit": 2,
|
|
3508
|
-
"remote-pair-receive":
|
|
3509
|
-
"remote-pair-roundtrip":
|
|
3510
|
-
"remote-pair-send":
|
|
3510
|
+
"remote-pair-receive": 10,
|
|
3511
|
+
"remote-pair-roundtrip": 9,
|
|
3512
|
+
"remote-pair-send": 21,
|
|
3511
3513
|
"remote-pair-transport": 4,
|
|
3512
3514
|
"retired-sent-surface": 2,
|
|
3513
3515
|
"resume-args": 6,
|
|
3514
3516
|
"resume-launch-identity": 10,
|
|
3515
3517
|
"self-address": 8,
|
|
3516
3518
|
"setup-verdict": 14,
|
|
3517
|
-
"source-install":
|
|
3519
|
+
"source-install": 8,
|
|
3518
3520
|
"typing-call-fence": 1,
|
|
3519
3521
|
"v2-surface": 14,
|
|
3520
3522
|
"v2-visible-resume": 40,
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
* - resolver fail-fast: resolveNativePushAdapter("antigravity") resolves; unknown throws.
|
|
19
19
|
*
|
|
20
20
|
* Pure; no backend, no socket, no real process.
|
|
21
|
+
*
|
|
22
|
+
* #152: this is the adapter LEAF only. Public agy delivery, sender replyability and
|
|
23
|
+
* registration are paused before any adapter is resolved; a green here is not agy
|
|
24
|
+
* availability and not a live pass.
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
import assert from "node:assert/strict";
|
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
* agy) and an ISOLATED temp meta-record store (never the real ~/.pi).
|
|
5
5
|
*
|
|
6
6
|
* Proves:
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
* garden id is engraved onto a pointer that does not resolve to a live host).
|
|
7
|
+
* - #152 PAUSE: antigravity — the only registerable backend — is refused by name BEFORE any
|
|
8
|
+
* adapter is resolved or probed, and NO record is written. While the pause holds, the create /
|
|
9
|
+
* attach / refuse-not-live mechanics behind that guard are unreachable, so this gate no longer
|
|
10
|
+
* claims them; they return with the pause's lift.
|
|
11
|
+
* - the paused-policy check sits BEFORE the probe in the register source.
|
|
13
12
|
* - RECEIVER-MARKER ABSTINENCE (보정①): the register source references NO receiver-marker
|
|
14
13
|
* writer — a native-push citizen has no idle-wake watch, so arming one would smuggle
|
|
15
14
|
* native-push liveness into the mailbox deliverability atom.
|
|
16
15
|
*
|
|
17
|
-
* Deterministic (
|
|
16
|
+
* Deterministic (trap adapter); the only IO is an isolated mkdtemp store (like the meta gates).
|
|
18
17
|
*/
|
|
19
18
|
|
|
20
19
|
import assert from "node:assert/strict";
|
|
@@ -22,8 +21,6 @@ import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
|
22
21
|
import * as os from "node:os";
|
|
23
22
|
import * as path from "node:path";
|
|
24
23
|
import { fileURLToPath } from "node:url";
|
|
25
|
-
import { readMetaIdentityByGardenId } from "../pi-extensions/lib/meta-session.ts";
|
|
26
|
-
import type { NativePushAdapter, NativePushProbeResult } from "../pi-extensions/lib/native-push/adapter.ts";
|
|
27
24
|
import { registerNativeConversation } from "../pi-extensions/lib/native-push/register.ts";
|
|
28
25
|
|
|
29
26
|
let passed = 0;
|
|
@@ -35,83 +32,31 @@ function ok(label: string, cond: boolean): void {
|
|
|
35
32
|
|
|
36
33
|
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
37
34
|
|
|
38
|
-
/** A fake native-push adapter whose probe returns a fixed result and whose send is unused. */
|
|
39
|
-
function fakeAdapter(probe: NativePushProbeResult): NativePushAdapter {
|
|
40
|
-
return {
|
|
41
|
-
id: "antigravity",
|
|
42
|
-
retriable: true,
|
|
43
|
-
async probe() {
|
|
44
|
-
return probe;
|
|
45
|
-
},
|
|
46
|
-
async send() {
|
|
47
|
-
throw new Error("register gate: adapter.send must not be called during registration");
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const ALIVE: NativePushProbeResult = {
|
|
53
|
-
status: "alive",
|
|
54
|
-
route: { backend: "antigravity", lsAddress: "127.0.0.1:5599" },
|
|
55
|
-
};
|
|
56
|
-
|
|
57
35
|
async function main(): Promise<void> {
|
|
58
36
|
const dir = mkdtempSync(path.join(os.tmpdir(), "entwurf-register-"));
|
|
59
37
|
try {
|
|
60
|
-
// ──
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const reattached = await registerNativeConversation(
|
|
78
|
-
{ backend: "antigravity", nativeSessionId: "conv-abc", cwd: "/work/two" },
|
|
79
|
-
{ resolveAdapter: () => fakeAdapter(ALIVE), sessionsDir: dir },
|
|
80
|
-
);
|
|
81
|
-
ok("attach: action = attach", reattached.action === "attach");
|
|
82
|
-
ok("attach: SAME garden id (no second mint)", reattached.gardenId === created.gardenId);
|
|
83
|
-
ok(
|
|
84
|
-
"attach: cwd REFRESHED to the new value",
|
|
85
|
-
reattached.gardenId === created.gardenId && reattached.cwd === "/work/two",
|
|
86
|
-
);
|
|
87
|
-
const rec2 = readMetaIdentityByGardenId(created.gardenId, dir);
|
|
88
|
-
ok("attach: on-disk cwd refreshed", rec2.cwd === "/work/two");
|
|
89
|
-
// exactly ONE record file for this conversation (no duplicate mint).
|
|
90
|
-
const records = readdirSync(dir).filter((f) => f.endsWith(".meta.json"));
|
|
91
|
-
ok("attach: exactly ONE meta-record (no duplicate garden id)", records.length === 1);
|
|
92
|
-
|
|
93
|
-
// ── not-live probe → REFUSE (throw), NO record written ────────────────────
|
|
94
|
-
for (const probe of [
|
|
95
|
-
{ status: "dead", reason: "no host" } as const,
|
|
96
|
-
{ status: "indeterminate", reason: "no port served" } as const,
|
|
97
|
-
]) {
|
|
98
|
-
const before = readdirSync(dir).length;
|
|
99
|
-
let threw = false;
|
|
100
|
-
try {
|
|
101
|
-
await registerNativeConversation(
|
|
102
|
-
{ backend: "antigravity", nativeSessionId: "conv-ghost", cwd: "/work/x" },
|
|
103
|
-
{ resolveAdapter: () => fakeAdapter(probe), sessionsDir: dir },
|
|
104
|
-
);
|
|
105
|
-
} catch (err) {
|
|
106
|
-
threw = true;
|
|
107
|
-
ok(
|
|
108
|
-
`refuse(${probe.status}): error names the non-live probe`,
|
|
109
|
-
new RegExp(probe.status).test((err as Error).message),
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
ok(`refuse(${probe.status}): THROWS (no garden id for a non-live conversation)`, threw);
|
|
113
|
-
ok(`refuse(${probe.status}): NO record written`, readdirSync(dir).length === before);
|
|
38
|
+
// ── #152: antigravity is PAUSED → REFUSE by name, no adapter, NO record ──────
|
|
39
|
+
let resolved = 0;
|
|
40
|
+
const before = readdirSync(dir).length;
|
|
41
|
+
let message = "";
|
|
42
|
+
try {
|
|
43
|
+
await registerNativeConversation(
|
|
44
|
+
{ backend: "antigravity", nativeSessionId: "conv-abc", cwd: "/work/one" },
|
|
45
|
+
{
|
|
46
|
+
resolveAdapter: () => {
|
|
47
|
+
resolved++;
|
|
48
|
+
throw new Error("register gate: no adapter may be resolved for a paused backend");
|
|
49
|
+
},
|
|
50
|
+
sessionsDir: dir,
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
message = (err as Error).message;
|
|
114
55
|
}
|
|
56
|
+
ok("paused: register THROWS for antigravity", message !== "");
|
|
57
|
+
ok("paused: the error names the pause (#152)", /paused/.test(message) && /#152/.test(message));
|
|
58
|
+
ok("paused: no adapter resolved (nothing probed)", resolved === 0);
|
|
59
|
+
ok("paused: NO record written", readdirSync(dir).length === before);
|
|
115
60
|
} finally {
|
|
116
61
|
rmSync(dir, { recursive: true, force: true });
|
|
117
62
|
}
|
|
@@ -127,6 +72,11 @@ async function main(): Promise<void> {
|
|
|
127
72
|
// it DOES reuse the upsert authority (봉인 5) and the adapter probe.
|
|
128
73
|
ok("wiring: register reuses upsertMetaSession", /upsertMetaSession/.test(src));
|
|
129
74
|
ok("wiring: register probes the adapter before minting", /\.probe\(/.test(src));
|
|
75
|
+
ok(
|
|
76
|
+
"wiring: the paused-policy check precedes the adapter probe",
|
|
77
|
+
src.indexOf("nativePushPaused(input.backend)") > 0 &&
|
|
78
|
+
src.indexOf("nativePushPaused(input.backend)") < src.indexOf(".probe("),
|
|
79
|
+
);
|
|
130
80
|
|
|
131
81
|
console.log(`\ncheck-native-push-register: ${passed} assertions passed`);
|
|
132
82
|
}
|
|
@@ -190,8 +190,8 @@ try {
|
|
|
190
190
|
);
|
|
191
191
|
const summary = (vitest.stdout.match(/Tests\s+.*$/m) ?? [""])[0].trim();
|
|
192
192
|
ok(
|
|
193
|
-
"[QK:RPI-ROUNDTRIP-ON-INSTALL] the T2 cells (A→B, reverse dispatch, opt-in absent off, other host refuses, both socket dirs) pass against the installed bridge and entry",
|
|
194
|
-
vitest.status === 0 && /^Tests\s+
|
|
193
|
+
"[QK:RPI-ROUNDTRIP-ON-INSTALL] the T2 cells (A→B, reverse dispatch, opt-in absent off, other host refuses, both socket dirs, and the #149 three-route cells: independent, reverse, undeclared sender, cross claim) pass against the installed bridge and entry",
|
|
194
|
+
vitest.status === 0 && /^Tests\s+9 passed\b/.test(summary),
|
|
195
195
|
`${summary}\n${vitest.stdout
|
|
196
196
|
.split("\n")
|
|
197
197
|
.filter((l) => /×|FAIL|Error/.test(l))
|
package/scripts/dev-bin.sh
CHANGED
|
@@ -34,6 +34,14 @@
|
|
|
34
34
|
# remove [name] honest inverse from state (remove only OUR link; REFUSE if it became foreign).
|
|
35
35
|
# no name = all managed bins.
|
|
36
36
|
# verify <name> require OUR symlink → target and that exact link as the PATH winner.
|
|
37
|
+
# exclusive require that NO other root answers to any package.json bin name — not only
|
|
38
|
+
# the PATH winner. A winner check alone let a later PATH entry (an old global
|
|
39
|
+
# shim) stay selectable by a shell with another PATH order, e.g. non-interactive
|
|
40
|
+
# SSH. Read-only: every entry of this PATH plus the global bin dir that `pnpm bin
|
|
41
|
+
# -g` / `npm prefix -g` report. Same resolved target = the same install (allowed);
|
|
42
|
+
# anything else is another root, never guessed as ours. exit 0 none / 5 another
|
|
43
|
+
# root (named, with a conditional removal hint — nothing is removed) / 6 the
|
|
44
|
+
# package manifest or a package manager could not be inspected (fail-closed).
|
|
37
45
|
#
|
|
38
46
|
# Overridable for the isolated smoke:
|
|
39
47
|
# ENTWURF_DEV_BIN_DIR link location (default: ~/.local/bin — where agy itself lives)
|
|
@@ -42,6 +50,7 @@
|
|
|
42
50
|
# ENTWURF_AGY_STATUSLINE_TARGET entwurf-agy-statusline target (default: $REPO/scripts/agy-statusline.sh)
|
|
43
51
|
# ENTWURF_AGY_IMPRINT_TARGET entwurf-agy-imprint target (default: $REPO/scripts/agy-imprint.sh)
|
|
44
52
|
# ENTWURF_COPILOT_STATUSLINE_TARGET entwurf-copilot-statusline target (default: $REPO/scripts/copilot-statusline.sh)
|
|
53
|
+
# ENTWURF_DEV_BIN_PM_TIMEOUT seconds one package-manager query may take (default 10)
|
|
45
54
|
# XDG_DATA_HOME state root (states under $XDG_DATA_HOME/entwurf/dev-bin/)
|
|
46
55
|
set -euo pipefail
|
|
47
56
|
|
|
@@ -68,6 +77,9 @@ bin_target() { # $1 = name → prints target path (env-overridable for smoke)
|
|
|
68
77
|
*) return 1 ;;
|
|
69
78
|
esac
|
|
70
79
|
}
|
|
80
|
+
# Every name bin_target answers for. Kept apart from MANAGED_BINS (what expose links): whether a
|
|
81
|
+
# name is exposed must not change the target exclusive expects for it.
|
|
82
|
+
TARGET_BINS="entwurf entwurf-bridge entwurf-agy-statusline entwurf-agy-imprint entwurf-copilot-statusline"
|
|
71
83
|
state_file_for() { echo "$STATE_DIR/$1.install-state.json"; }
|
|
72
84
|
|
|
73
85
|
state_link() { # $1 = state file → recorded linkPath, or empty
|
|
@@ -184,6 +196,104 @@ verify_one() { # $1 = name
|
|
|
184
196
|
log "[dev-bin verify] $name → $target (PATH winner: $link)"
|
|
185
197
|
}
|
|
186
198
|
|
|
199
|
+
do_exclusive() {
|
|
200
|
+
# Our target per package.json bin name: a managed name through bin_target (smoke-overridable),
|
|
201
|
+
# every other name at its package.json path in this checkout. The manifest is read INSIDE the
|
|
202
|
+
# verdict step: a read that fails outside it would reach the verdict as zero names and pass.
|
|
203
|
+
local -a pairs=()
|
|
204
|
+
local b
|
|
205
|
+
for b in $TARGET_BINS; do pairs+=("$b=$(bin_target "$b")"); done
|
|
206
|
+
python3 - "${ENTWURF_DEV_BIN_PM_TIMEOUT:-10}" "$REPO_DIR" "${pairs[@]}" <<'PY'
|
|
207
|
+
import json, os, shutil, signal, subprocess, sys
|
|
208
|
+
|
|
209
|
+
timeout = float(sys.argv[1])
|
|
210
|
+
manifest = os.path.join(sys.argv[2], "package.json")
|
|
211
|
+
try:
|
|
212
|
+
with open(manifest) as f:
|
|
213
|
+
bins = json.load(f)["bin"]
|
|
214
|
+
if not (isinstance(bins, dict) and "entwurf" in bins
|
|
215
|
+
and all(isinstance(k, str) and k and isinstance(v, str) and v for k, v in bins.items())):
|
|
216
|
+
raise ValueError('"bin" is not a non-empty object of name -> path strings naming entwurf')
|
|
217
|
+
except (OSError, ValueError, KeyError, TypeError) as e:
|
|
218
|
+
print(f"[dev-bin] FAIL: could not inspect the package manifest {manifest} — {type(e).__name__}: {e}."
|
|
219
|
+
" Not read as 'no other root'.", file=sys.stderr)
|
|
220
|
+
print(" nothing was removed, no profile edited, no session stopped.", file=sys.stderr)
|
|
221
|
+
sys.exit(6) # an unreadable manifest is never "0 names, no other root"
|
|
222
|
+
managed = dict(pair.split("=", 1) for pair in sys.argv[3:])
|
|
223
|
+
ours = {name: os.path.realpath(managed.get(name, os.path.join(sys.argv[2], rel)))
|
|
224
|
+
for name, rel in bins.items()}
|
|
225
|
+
|
|
226
|
+
dirs = {} # realpath(dir) -> (dir as seen, [where it was seen])
|
|
227
|
+
def add(d, where):
|
|
228
|
+
key = os.path.realpath(d)
|
|
229
|
+
dirs.setdefault(key, (d, []))[1].append(where)
|
|
230
|
+
|
|
231
|
+
for d in os.environ.get("PATH", "").split(":"):
|
|
232
|
+
add(d or os.getcwd(), "PATH") # an empty POSIX PATH entry means the cwd
|
|
233
|
+
|
|
234
|
+
def query(argv):
|
|
235
|
+
exe = shutil.which(argv[0])
|
|
236
|
+
if exe is None:
|
|
237
|
+
return "skip", f"{argv[0]} not on PATH — its global bin dir was not checked"
|
|
238
|
+
try:
|
|
239
|
+
p = subprocess.Popen([exe] + argv[1:], cwd="/", stdin=subprocess.DEVNULL,
|
|
240
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=True)
|
|
241
|
+
except OSError as e:
|
|
242
|
+
return "error", f"`{' '.join(argv)}` could not start: {e}"
|
|
243
|
+
try:
|
|
244
|
+
out, err = p.communicate(timeout=timeout)
|
|
245
|
+
except subprocess.TimeoutExpired:
|
|
246
|
+
os.killpg(p.pid, signal.SIGKILL)
|
|
247
|
+
p.communicate()
|
|
248
|
+
return "error", f"`{' '.join(argv)}` did not answer within {timeout:g}s"
|
|
249
|
+
if p.returncode != 0:
|
|
250
|
+
tail = err.decode(errors="replace").strip().splitlines()[-1:] or [""]
|
|
251
|
+
return "error", f"`{' '.join(argv)}` exited {p.returncode}: {tail[0]}"
|
|
252
|
+
lines = out.decode(errors="replace").strip().splitlines()
|
|
253
|
+
if len(lines) != 1 or not os.path.isabs(lines[0]):
|
|
254
|
+
return "error", f"`{' '.join(argv)}` printed {len(lines)} line(s), not one absolute path"
|
|
255
|
+
return "ok", lines[0]
|
|
256
|
+
|
|
257
|
+
checked, skipped, errors = ["PATH"], [], []
|
|
258
|
+
for argv, label, to_bin in ((["pnpm", "bin", "-g"], "pnpm global bin", lambda s: s),
|
|
259
|
+
(["npm", "prefix", "-g"], "npm global bin", lambda s: os.path.join(s, "bin"))):
|
|
260
|
+
state, value = query(argv)
|
|
261
|
+
if state == "ok":
|
|
262
|
+
add(to_bin(value), label)
|
|
263
|
+
checked.append(f"{label} {to_bin(value)}")
|
|
264
|
+
elif state == "skip":
|
|
265
|
+
skipped.append(value)
|
|
266
|
+
else:
|
|
267
|
+
errors.append(value)
|
|
268
|
+
|
|
269
|
+
others = []
|
|
270
|
+
for _key, (d, where) in dirs.items():
|
|
271
|
+
for name, target in ours.items():
|
|
272
|
+
path = os.path.join(d, name)
|
|
273
|
+
if os.path.lexists(path) and os.path.realpath(path) != target:
|
|
274
|
+
others.append((path, os.path.realpath(path), where))
|
|
275
|
+
|
|
276
|
+
for path, resolved, where in others:
|
|
277
|
+
print(f"[dev-bin] FAIL: another entwurf root is still selectable: {path} -> {resolved}", file=sys.stderr)
|
|
278
|
+
print(f" seen via: {', '.join(where)}", file=sys.stderr)
|
|
279
|
+
hints = [h for w, h in (("pnpm global bin", "pnpm remove -g @junghanacs/entwurf"),
|
|
280
|
+
("npm global bin", "npm uninstall -g @junghanacs/entwurf")) if w in where]
|
|
281
|
+
for h in hints:
|
|
282
|
+
print(f" if a global @junghanacs/entwurf installed it: {h}", file=sys.stderr)
|
|
283
|
+
print(" " + ("otherwise " if hints else "") + "remove it through whoever owns it.", file=sys.stderr)
|
|
284
|
+
for e in errors:
|
|
285
|
+
print(f"[dev-bin] FAIL: could not inspect a package manager — {e}. Not read as 'no other root'.", file=sys.stderr)
|
|
286
|
+
if others or errors:
|
|
287
|
+
print(f" this checkout: {ours.get('entwurf', '?')}", file=sys.stderr)
|
|
288
|
+
print(" nothing was removed, no profile edited, no session stopped. Then re-run: ./run.sh setup <project>", file=sys.stderr)
|
|
289
|
+
sys.exit(5 if others else 6)
|
|
290
|
+
print(f"[dev-bin exclusive] no other root under the {len(ours)} package bin names in: {'; '.join(checked)}")
|
|
291
|
+
for s in skipped:
|
|
292
|
+
print(f" not checked: {s}")
|
|
293
|
+
print(" covers only these directories — another shell's PATH or another package manager is not covered.")
|
|
294
|
+
PY
|
|
295
|
+
}
|
|
296
|
+
|
|
187
297
|
remove_one() { # $1 = name
|
|
188
298
|
local name="$1" target link sf recorded
|
|
189
299
|
target="$(bin_target "$name")" || fail "unknown managed bin: $name"
|
|
@@ -235,5 +345,6 @@ case "${1:-}" in
|
|
|
235
345
|
expose) shift; do_expose "${1:-}" ;;
|
|
236
346
|
remove) shift; do_remove "${1:-}" ;;
|
|
237
347
|
verify) shift; [ -n "${1:-}" ] || fail "verify requires one managed bin name"; verify_one "$1" ;;
|
|
238
|
-
|
|
348
|
+
exclusive) do_exclusive ;;
|
|
349
|
+
*) echo "usage: dev-bin.sh <expose|remove> [bin-name] | verify <bin-name> | exclusive" >&2; exit 2 ;;
|
|
239
350
|
esac
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminus wait of `smoke-entwurf-chain-live`, as one function with its fs seams injectable, so
|
|
3
|
+
* the oracle that judges a live chain can be driven offline through the exact publication states.
|
|
4
|
+
*
|
|
5
|
+
* `enqueueMetaMessage` (pi-extensions/lib/meta-session.ts) publishes in two writes: the `.msg` body
|
|
6
|
+
* FIRST, then — after the state stamp — `inbox.signal` LAST. A reader can therefore see the body
|
|
7
|
+
* without the signal for a few milliseconds. The P5 failure on 2026-10-24 is consistent with that by
|
|
8
|
+
* source and by its preserved mailbox: the old wait stopped at the first body and checked the signal
|
|
9
|
+
* at once, and the signal file was written about 5 ms after the body. Its interleaving was not traced.
|
|
10
|
+
* A body without the signal is a publication still in flight, so the wait ends only when both are
|
|
11
|
+
* there, inside the same deadline. What the smoke asserts afterwards (exactly one body, the signal,
|
|
12
|
+
* every identity and read-receipt check) is unchanged and still fails on a missing body, a missing
|
|
13
|
+
* signal or a duplicate.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import fsp from "node:fs/promises";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
export interface TerminusPublicationIo {
|
|
20
|
+
readdir(dir: string): Promise<string[]>;
|
|
21
|
+
exists(file: string): boolean;
|
|
22
|
+
sleep(ms: number): Promise<void>;
|
|
23
|
+
now(): number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const realIo: TerminusPublicationIo = {
|
|
27
|
+
readdir: (dir) => fsp.readdir(dir),
|
|
28
|
+
exists: (file) => existsSync(file),
|
|
29
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
30
|
+
now: () => Date.now(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export interface TerminusPublication {
|
|
34
|
+
/** Every `.msg` / `.msg.delivered` seen at the last look, so a duplicate stays visible. */
|
|
35
|
+
msgs: string[];
|
|
36
|
+
/** Whether `inbox.signal` existed at the last look. */
|
|
37
|
+
signalled: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Wait until the terminus mailbox holds a body AND its doorbell, or the deadline passes. */
|
|
41
|
+
export async function awaitTerminusPublication(
|
|
42
|
+
boxDir: string,
|
|
43
|
+
untilMs: number,
|
|
44
|
+
pollMs: number,
|
|
45
|
+
io: TerminusPublicationIo = realIo,
|
|
46
|
+
): Promise<TerminusPublication> {
|
|
47
|
+
let msgs: string[] = [];
|
|
48
|
+
let signalled = false;
|
|
49
|
+
while (io.now() < untilMs) {
|
|
50
|
+
msgs = (await io.readdir(boxDir).catch(() => [] as string[])).filter(
|
|
51
|
+
(f) => f.endsWith(".msg") || f.endsWith(".msg.delivered"),
|
|
52
|
+
);
|
|
53
|
+
signalled = io.exists(path.join(boxDir, "inbox.signal"));
|
|
54
|
+
if (msgs.length > 0 && signalled) break;
|
|
55
|
+
await io.sleep(pollMs);
|
|
56
|
+
}
|
|
57
|
+
return { msgs, signalled };
|
|
58
|
+
}
|