@monotykamary/pi-ledger 0.2.0 → 0.4.0
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/README.md +6 -2
- package/extensions/pi-ledger/__tests__/helpers.ts +8 -1
- package/extensions/pi-ledger/__tests__/in-process-session-observer.test.ts +555 -0
- package/extensions/pi-ledger/__tests__/ledger.test.ts +125 -0
- package/extensions/pi-ledger/__tests__/nested-agent-telemetry.test.ts +867 -0
- package/extensions/pi-ledger/__tests__/subprocess-observer.test.ts +364 -0
- package/extensions/pi-ledger/in-process-session-observer.ts +890 -0
- package/extensions/pi-ledger/index.ts +121 -1
- package/extensions/pi-ledger/nested-agent-telemetry.ts +1271 -0
- package/extensions/pi-ledger/subprocess-observer.ts +694 -0
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
fmtMoney,
|
|
24
24
|
rehydrateFromSidecar,
|
|
25
25
|
resolveExtensionBudget,
|
|
26
|
+
retryEventId,
|
|
26
27
|
sidecarPathFor,
|
|
27
28
|
type LedgerSettings,
|
|
28
29
|
type ReceiptData,
|
|
@@ -2526,3 +2527,127 @@ describe('rehydrateFromSidecar — skip-billing guard', () => {
|
|
|
2526
2527
|
expect(rehydrateFromSidecar([]).billingPaused).toBe(false);
|
|
2527
2528
|
});
|
|
2528
2529
|
});
|
|
2530
|
+
|
|
2531
|
+
// ─── pi-retry capture (wizard gating) ────────────────────────────────────────
|
|
2532
|
+
// The engagement prompt (no rolling credit) lives at agent_settled. When
|
|
2533
|
+
// @monotykamary/pi-retry is installed, agent_settled can fire during a retry's
|
|
2534
|
+
// backoff sleep — before pi-retry has decided whether to re-prompt. pi-ledger
|
|
2535
|
+
// captures pi-retry's started/completed/cancelled events and DEFERS the prompt
|
|
2536
|
+
// until the retry settles, so the backoff is never billed as human time.
|
|
2537
|
+
// Mirrors localterm's agent-notify contract.
|
|
2538
|
+
|
|
2539
|
+
describe('pi-retry capture (wizard gating)', () => {
|
|
2540
|
+
let fixture: TestFixture;
|
|
2541
|
+
let cacheDir: string;
|
|
2542
|
+
|
|
2543
|
+
beforeEach(async () => {
|
|
2544
|
+
vi.useFakeTimers();
|
|
2545
|
+
cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-ledger-test-'));
|
|
2546
|
+
process.env.XDG_CACHE_HOME = cacheDir;
|
|
2547
|
+
fixture = createTestFixture();
|
|
2548
|
+
await activateExtension(fixture);
|
|
2549
|
+
});
|
|
2550
|
+
|
|
2551
|
+
afterEach(() => {
|
|
2552
|
+
vi.useRealTimers();
|
|
2553
|
+
delete process.env.XDG_CACHE_HOME;
|
|
2554
|
+
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2557
|
+
const STARTED = 'pi-retry:started';
|
|
2558
|
+
const COMPLETED = 'pi-retry:completed';
|
|
2559
|
+
const CANCELLED = 'pi-retry:cancelled';
|
|
2560
|
+
|
|
2561
|
+
it('pops the wizard at agent_settled when no pi-retry is active', () => {
|
|
2562
|
+
fixture.run('session_start', { type: 'session_start', reason: 'startup' });
|
|
2563
|
+
fixture.run('agent_end', { type: 'agent_end', messages: [] });
|
|
2564
|
+
expect(fixture.customSpy).not.toHaveBeenCalled();
|
|
2565
|
+
fixture.run('agent_settled', { type: 'agent_settled' });
|
|
2566
|
+
expect(fixture.customSpy).toHaveBeenCalledTimes(1); // no credit → pop
|
|
2567
|
+
});
|
|
2568
|
+
|
|
2569
|
+
it('defers the wizard while a pi-retry is in flight; pops on completed', () => {
|
|
2570
|
+
fixture.run('session_start', { type: 'session_start', reason: 'startup' });
|
|
2571
|
+
fixture.run('agent_start', { type: 'agent_start' });
|
|
2572
|
+
fixture.run('agent_end', { type: 'agent_end', messages: [] });
|
|
2573
|
+
fixture.emitEvent(STARTED, { retryId: 1 });
|
|
2574
|
+
fixture.run('agent_settled', { type: 'agent_settled' }); // retry in flight → defer
|
|
2575
|
+
expect(fixture.customSpy).not.toHaveBeenCalled();
|
|
2576
|
+
fixture.emitEvent(COMPLETED, { retryId: 1 }); // retry settled → pop the deferred prompt
|
|
2577
|
+
expect(fixture.customSpy).toHaveBeenCalledTimes(1);
|
|
2578
|
+
expect(fixture.lastSidecarEvent('human-open')).toBeUndefined(); // still engagement-gated
|
|
2579
|
+
});
|
|
2580
|
+
|
|
2581
|
+
it('waits for the final agent_settled when retry completion arrives first', () => {
|
|
2582
|
+
fixture.run('session_start', { type: 'session_start', reason: 'startup' });
|
|
2583
|
+
fixture.run('agent_start', { type: 'agent_start' });
|
|
2584
|
+
fixture.run('agent_end', { type: 'agent_end', messages: [] });
|
|
2585
|
+
fixture.emitEvent(STARTED, { retryId: 2 });
|
|
2586
|
+
fixture.run('agent_settled', { type: 'agent_settled' }); // retry in flight → defer
|
|
2587
|
+
// the retry turn fires (a new run supersedes the stale deferred prompt)
|
|
2588
|
+
fixture.run('agent_start', { type: 'agent_start' });
|
|
2589
|
+
fixture.run('agent_end', { type: 'agent_end', messages: [] });
|
|
2590
|
+
// retry completes BEFORE the run settles → pending was cleared by agent_start → no pop
|
|
2591
|
+
fixture.emitEvent(COMPLETED, { retryId: 2 });
|
|
2592
|
+
expect(fixture.customSpy).not.toHaveBeenCalled();
|
|
2593
|
+
fixture.run('agent_settled', { type: 'agent_settled' }); // now settled, no retry active → pop
|
|
2594
|
+
expect(fixture.customSpy).toHaveBeenCalledTimes(1);
|
|
2595
|
+
});
|
|
2596
|
+
|
|
2597
|
+
it('never pops when a pi-retry is cancelled', () => {
|
|
2598
|
+
fixture.run('session_start', { type: 'session_start', reason: 'startup' });
|
|
2599
|
+
fixture.run('agent_start', { type: 'agent_start' });
|
|
2600
|
+
fixture.run('agent_end', { type: 'agent_end', messages: [] });
|
|
2601
|
+
fixture.emitEvent(STARTED, { retryId: 3 });
|
|
2602
|
+
fixture.run('agent_settled', { type: 'agent_settled' }); // retry in flight → defer
|
|
2603
|
+
expect(fixture.customSpy).not.toHaveBeenCalled();
|
|
2604
|
+
fixture.emitEvent(CANCELLED, { retryId: 3 }); // abort/session-change → drop the prompt
|
|
2605
|
+
expect(fixture.customSpy).not.toHaveBeenCalled();
|
|
2606
|
+
});
|
|
2607
|
+
|
|
2608
|
+
it('ignores a completed event that does not match the active retry', () => {
|
|
2609
|
+
fixture.run('session_start', { type: 'session_start', reason: 'startup' });
|
|
2610
|
+
fixture.run('agent_start', { type: 'agent_start' });
|
|
2611
|
+
fixture.run('agent_end', { type: 'agent_end', messages: [] });
|
|
2612
|
+
fixture.emitEvent(STARTED, { retryId: 4 });
|
|
2613
|
+
fixture.run('agent_settled', { type: 'agent_settled' }); // defer (active id 4)
|
|
2614
|
+
fixture.emitEvent(COMPLETED, { retryId: 999 }); // stray → ignored
|
|
2615
|
+
expect(fixture.customSpy).not.toHaveBeenCalled();
|
|
2616
|
+
fixture.emitEvent(COMPLETED, { retryId: 4 }); // matching → pop
|
|
2617
|
+
expect(fixture.customSpy).toHaveBeenCalledTimes(1);
|
|
2618
|
+
});
|
|
2619
|
+
|
|
2620
|
+
it('treats a completed with no prior started as a no-op', () => {
|
|
2621
|
+
fixture.run('session_start', { type: 'session_start', reason: 'startup' });
|
|
2622
|
+
fixture.emitEvent(COMPLETED, { retryId: 7 }); // no retry active → stray event
|
|
2623
|
+
expect(fixture.customSpy).not.toHaveBeenCalled();
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
it('defers auto-extend too while a pi-retry is in flight; extends on completed', () => {
|
|
2627
|
+
fixture.seedSidecar([
|
|
2628
|
+
{ kind: 'settings', settings: { ...DEFAULTS, autoExtend: true }, timestamp: 0 },
|
|
2629
|
+
]);
|
|
2630
|
+
fixture.run('session_start', { type: 'session_start', reason: 'startup' }); // rehydrate → autoExtend on
|
|
2631
|
+
fixture.run('agent_start', { type: 'agent_start' });
|
|
2632
|
+
fixture.run('agent_end', { type: 'agent_end', messages: [] });
|
|
2633
|
+
fixture.emitEvent(STARTED, { retryId: 5 });
|
|
2634
|
+
fixture.run('agent_settled', { type: 'agent_settled' }); // retry in flight → defer auto-extend
|
|
2635
|
+
expect(fixture.readSidecarEvents().filter((e) => e.kind === 'human-open')).toHaveLength(0);
|
|
2636
|
+
fixture.emitEvent(COMPLETED, { retryId: 5 }); // settled → auto-extend provisions a block + engages
|
|
2637
|
+
const open = fixture.lastSidecarEvent('human-open');
|
|
2638
|
+
expect(open).toBeDefined();
|
|
2639
|
+
expect(open!.engagedVia).toBe('extension'); // auto-extend engages via extension
|
|
2640
|
+
});
|
|
2641
|
+
});
|
|
2642
|
+
|
|
2643
|
+
describe('retryEventId', () => {
|
|
2644
|
+
it('returns the retryId of a pi-retry event', () => {
|
|
2645
|
+
expect(retryEventId({ retryId: 42 })).toBe(42);
|
|
2646
|
+
});
|
|
2647
|
+
it('returns undefined when retryId is absent or malformed', () => {
|
|
2648
|
+
expect(retryEventId(undefined)).toBeUndefined();
|
|
2649
|
+
expect(retryEventId(null)).toBeUndefined();
|
|
2650
|
+
expect(retryEventId({})).toBeUndefined();
|
|
2651
|
+
expect(retryEventId({ retryId: 'oops' })).toBeUndefined();
|
|
2652
|
+
});
|
|
2653
|
+
});
|