@aexol/spectral 0.4.9 → 0.4.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.
|
@@ -42,6 +42,13 @@ import { generateSessionTitle, isDefaultTitle, } from "./title-generator.js";
|
|
|
42
42
|
const DEFAULT_BRIDGE_FACTORY = (args) => new PiBridge(args);
|
|
43
43
|
/** Safety limit for autonomous loop iterations per session. */
|
|
44
44
|
const MAX_LOOP_ITERATIONS = 100;
|
|
45
|
+
/**
|
|
46
|
+
* Token threshold above which the loop triggers compaction between iterations.
|
|
47
|
+
* Aligned with the observational memory extension default (memory/config.ts).
|
|
48
|
+
*/
|
|
49
|
+
const LOOP_COMPACTION_THRESHOLD_TOKENS = 50_000;
|
|
50
|
+
/** Maximum time (ms) to wait for compaction to finish between loop iterations. */
|
|
51
|
+
const LOOP_COMPACTION_MAX_WAIT_MS = 30_000;
|
|
45
52
|
/**
|
|
46
53
|
* Number of accumulated wire events before flushing the in-flight turn
|
|
47
54
|
* to SQLite. Batch-persisting means a server crash mid-turn only loses
|
|
@@ -524,6 +531,58 @@ export class SessionStreamManager {
|
|
|
524
531
|
}
|
|
525
532
|
});
|
|
526
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* Send the next prompt for an autonomous loop iteration, waiting for any
|
|
536
|
+
* in-flight observational-memory compaction to finish first so the LLM
|
|
537
|
+
* sees the compacted context.
|
|
538
|
+
*
|
|
539
|
+
* The compaction-trigger extension fires on agent_end (via setTimeout),
|
|
540
|
+
* so we poll stream.compacting briefly. If compaction hasn't started after
|
|
541
|
+
* a short grace period, we check whether the context window exceeds the
|
|
542
|
+
* threshold and proactively trigger compaction via the bridge (which
|
|
543
|
+
* invokes pi's full pipeline, including the session_before_compact hook
|
|
544
|
+
* where the observational memory extension provides its summary).
|
|
545
|
+
*
|
|
546
|
+
* A duplicate call from the extension's delayed compaction trigger is
|
|
547
|
+
* harmless — pi throws "Already compacted" which the extension catches.
|
|
548
|
+
*/
|
|
549
|
+
async sendNextLoopIteration(stream) {
|
|
550
|
+
// The observational-memory extension's compaction-trigger fires on
|
|
551
|
+
// agent_end (via setTimeout, so it's slightly delayed). We wait for
|
|
552
|
+
// either:
|
|
553
|
+
// (a) compaction to start and finish (stream.compacting becomes
|
|
554
|
+
// true then false), or
|
|
555
|
+
// (b) a grace period to elapse without compaction starting (context
|
|
556
|
+
// was below the threshold, or the extension deferred).
|
|
557
|
+
//
|
|
558
|
+
// This guarantees the LLM sees the compacted context when the
|
|
559
|
+
// compaction threshold was reached, and doesn't delay the loop when
|
|
560
|
+
// no compaction is needed.
|
|
561
|
+
const GRACE_MS = 500; // give the trigger's setTimeout a chance
|
|
562
|
+
const start = Date.now();
|
|
563
|
+
// Wait for the trigger to start compaction, or for grace period to pass.
|
|
564
|
+
while (!stream.compacting && Date.now() - start < GRACE_MS) {
|
|
565
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
566
|
+
}
|
|
567
|
+
if (stream.compacting) {
|
|
568
|
+
console.log("[loop] compaction in progress, waiting...");
|
|
569
|
+
await this.waitForCompactionOrTimeout(stream);
|
|
570
|
+
console.log("[loop] compaction finished, sending next iteration");
|
|
571
|
+
}
|
|
572
|
+
await this.prompt(stream.sessionId, stream.loopOriginalPrompt, undefined);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Poll until stream.compacting becomes false or the timeout elapses.
|
|
576
|
+
*/
|
|
577
|
+
async waitForCompactionOrTimeout(stream) {
|
|
578
|
+
const start = Date.now();
|
|
579
|
+
while (stream.compacting && Date.now() - start < LOOP_COMPACTION_MAX_WAIT_MS) {
|
|
580
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
581
|
+
}
|
|
582
|
+
if (stream.compacting) {
|
|
583
|
+
console.warn(`[loop] compaction still in-flight after ${LOOP_COMPACTION_MAX_WAIT_MS}ms, proceeding anyway`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
527
586
|
// --- internals ----------------------------------------------------------
|
|
528
587
|
createStream(sessionId, history) {
|
|
529
588
|
// Resolve cwd from the owning project. Sessions without a project
|
|
@@ -555,6 +614,7 @@ export class SessionStreamManager {
|
|
|
555
614
|
loopOriginalPrompt: null,
|
|
556
615
|
forkCompactSourceId: forkSourceId ?? null,
|
|
557
616
|
compacting: false,
|
|
617
|
+
compactionEndResolve: null,
|
|
558
618
|
contextWindowUsed: null,
|
|
559
619
|
contextWindowMax: null,
|
|
560
620
|
};
|
|
@@ -703,6 +763,18 @@ export class SessionStreamManager {
|
|
|
703
763
|
if (event.contextWindowMax != null)
|
|
704
764
|
stream.contextWindowMax = event.contextWindowMax;
|
|
705
765
|
}
|
|
766
|
+
// Track compaction lifecycle so the loop (and prompt() guard) can wait
|
|
767
|
+
// for it to complete before starting the next iteration.
|
|
768
|
+
if (event.type === "compaction_start") {
|
|
769
|
+
stream.compacting = true;
|
|
770
|
+
}
|
|
771
|
+
if (event.type === "compaction_end") {
|
|
772
|
+
stream.compacting = false;
|
|
773
|
+
if (stream.compactionEndResolve) {
|
|
774
|
+
stream.compactionEndResolve();
|
|
775
|
+
stream.compactionEndResolve = null;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
706
778
|
this.broadcast(stream, event);
|
|
707
779
|
if (event.type === "agent_end") {
|
|
708
780
|
// Final flush + clear batch-persist tracking. `onAssistantMessageComplete`
|
|
@@ -753,7 +825,7 @@ export class SessionStreamManager {
|
|
|
753
825
|
maxIterations: MAX_LOOP_ITERATIONS,
|
|
754
826
|
prompt: stream.loopOriginalPrompt,
|
|
755
827
|
});
|
|
756
|
-
void this.
|
|
828
|
+
void this.sendNextLoopIteration(stream).catch((err) => {
|
|
757
829
|
console.error(`[loop] iteration failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
758
830
|
stream.loopActive = false;
|
|
759
831
|
stream.loopOriginalPrompt = null;
|