feat(index): preserve responsiveness during root indexing - #283
feat(index): preserve responsiveness during root indexing#283randomvariable wants to merge 14 commits into
Conversation
Bound interactive and maintenance queues, carry request deadlines across the SubC transport, and yield standing-root maintenance when cold-build capacity is saturated. Demote maintenance workers so reader latency remains stable under index load. Signed-off-by: Naadir Jeewa <naadir@randomvariable.co.uk>
|
Thank you - this lands squarely on planes we actively harden, so it will get a thorough review rather than a fast one. Initial scope notes before the deep pass: (1) main moved tonight in exactly this area - the executor's per-actor admission accounting changed (maintenance no longer consumes interactive capacity) and several of the test files you touch were rewritten to event-based assertion form - so a rebase onto current main is worth doing early; expect conceptual interplay between your queue bounds and the new accounting. (2) This bundles three separable features (queue bounding/backpressure, cross-path request budget carrying, maintenance yield/demotion) - if review friction builds, splitting them in that order would let the first land fastest, but we will review as-is first. (3) Concurrency-core changes here go through adversarial review including storm suites on loaded runners - the release storm gate in your test plan is the right instinct. Full review follows. |
Signed-off-by: Naadir Jeewa <naadir@randomvariable.co.uk> Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
7436084 to
a2e1d8a
Compare
Use pressure-aware deficit round robin to rotate standing roots across bounded search, semantic, and callgraph work. Preserve laptop responsiveness by default while allowing an explicit performance policy. Signed-off-by: Naadir Jeewa <naadir@randomvariable.co.uk> Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
Bound Pi-facing requests below the host deadline, move allocator scans off the transport thread, and avoid repeated standing-root reconciliation. Signed-off-by: Naadir Jeewa <naadir@randomvariable.co.uk>
Signed-off-by: Naadir Jeewa <naadir@randomvariable.co.uk> Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
There was a problem hiding this comment.
6 issues found across 64 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/aft/src/semantic_index.rs">
<violation number="1" location="crates/aft/src/semantic_index.rs:2859">
P2: Every standing slice repeats embedding-model setup and fingerprint probing, so a large root reloads FastEmbed or sends a remote probe on each scheduler slice. Retain the model and resolved fingerprint across slices, or persist the resolved dimension with the staging state.</violation>
<violation number="2" location="crates/aft/src/semantic_index.rs:2860">
P2: Each slice performs an unbounded full-corpus metadata scan before collecting only 32 files, so large roots can saturate I/O and defeat the responsiveness goal. Persist the inventory/fingerprint with the staging state and update it incrementally instead of rescanning every slice.</violation>
<violation number="3" location="crates/aft/src/semantic_index.rs:2863">
P2: For a borrow-only root, this creates and checkpoints `semantic-staging-v1.json` even though the final artifact write is denied. Check `ArtifactAccess::allows_write` before creating the directory or writing any staging state.</violation>
<violation number="4" location="crates/aft/src/semantic_index.rs:2986">
P1: When a standing build overlaps the normal semantic build for the same root, this path can publish an older snapshot after the newer build. Acquire the shared semantic writer lock or add a publication-generation fence covering staging and final publication.</violation>
</file>
<file name="crates/aft/src/callgraph_store/mod.rs">
<violation number="1" location="crates/aft/src/callgraph_store/mod.rs:2945">
P1: Nothing in this repository calls `resume_cold_build_slice_with_lease`, so the existing callgraph background paths never receive `ColdBuildSlice` progress and still run whole builds. Wire this API into the background scheduler before relying on the responsiveness change.</violation>
</file>
<file name="crates/aft/src/cold_build_limiter.rs">
<violation number="1" location="crates/aft/src/cold_build_limiter.rs:166">
P2: When a standing slice races a queued inspect or maintenance build for the final slot, this API can admit standing work ahead of the queued request. Preserve the pre- and post-acquisition `has_non_standing_waiters` checks in the immediate Standing path so standing work yields to already-queued non-standing work.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Signed-off-by: Naadir Jeewa <naadir@randomvariable.co.uk>
- Retire removed standing actors on session-bind reconciliation - Publish admission decisions to scheduler telemetry each tick - Fence standing semantic publication through WriterLease and admission epoch - Build standing callgraph slices via resume_cold_build_slice_with_lease - Admit balanced indexing when portable resource signals are unavailable - Treat unreadable battery capacity as Unknown power, not Battery - Preserve arrival order when pruning elapsed deadline jobs - Derive DRR reconciliation membership from one HashSet - Pass the bounded foreground window to Pi kill-timeout coherence Assisted-by: Claude Opus 4.5 Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
… publishers - bash: pass client-bounded foreground wait (foreground_wait_ms) from Pi through translate into select_foreground_wait_window_ms so the Rust promotion window cannot exceed the client request budget - subc bash: promote still-running commands and retire wait-mode bookkeeping when a poll phase is rejected after the request deadline instead of leaking registrations and returning a bare deadline error - callgraph: hoist method-dispatch edge insertion out of the publication transaction into durable per-slice chunks keyed by staged cursor keys so slice-budget exhaustion can no longer roll back dispatch work and wedge the build in an infinite resume loop - configure: acquire the root-keyed Index WriterLease around session search and semantic persistence so standing-root and session builds cannot interleave publications over the same cache directories - search: mid-build slices resume from the staged manifest without rewalking and refingerprinting the whole corpus; the publication slice revalidates the corpus and re-hashes staged contents so same-size mtime-preserving edits restart the build instead of publishing stale postings; spill file ids are allocated over included entries only to match publication compaction Signed-off-by: Naadir Sheriffdean <naadir@randomvariable.co.uk> Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
…ity, docs - callgraph fixture copy handles nested directories (recursive helper) - restore pub mod symbol_diff dropped from lib.rs (776 lines dead code) - executor test covers global interactive backpressure with second actor - retirement test proves no process-capacity leak via new-actor admission - deadline-elapsed test syncs on job start (removes submit/dispatch race) - halfway deadline case labeled for true 12s-budget halfway point - thread_priority: macOS warning carries rc; Windows formats last_os_error (was std::process::id()); BackgroundGuard restores prior state not default - docs: index.resource_policy marked USER-only in config snippet + prose Signed-off-by: Naadir Sheriffdean <naadir@randomvariable.co.uk> Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
Signed-off-by: Naadir Jeewa <naadir@randomvariable.co.uk> Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
|
Addressed the Greptile stale-generation finding and all 39 Cubic findings in commits 208d757..a6f3afb. Verification: Rust library 2,949 passed; integration 1,642 passed; release storm 16/16 passed; TypeScript transport/plugin 94/94; cargo check, rustfmt, and diff-check clean. Please re-review the current head. |
There was a problem hiding this comment.
1 existing issue remains and 3 new issues found across 24 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/aft/src/commands/configure.rs">
<violation number="1" location="crates/aft/src/commands/configure.rs:3409">
P2: When a standing worker and the session configure path publish the same root in one process, this shared lease does not serialize their writes because `WriterLease::acquire_shared` reuses the existing process-local lease. Add a per-cache publication mutex or an exclusive writer acquisition that both search and semantic publication paths hold through the final rename.</violation>
</file>
<file name="crates/aft/src/subc/standing.rs">
<violation number="1" location="crates/aft/src/subc/standing.rs:105">
P2: When a semantic build is cancelled or its root is removed, this actor retains its file inventory and embedding model indefinitely. Prune semantic cache entries during reconciliation and cancellation, not only after successful completion.</violation>
</file>
<file name="crates/aft/src/standing_scheduler.rs">
<violation number="1" location="crates/aft/src/standing_scheduler.rs:10">
P2: Every root ever passed to `reconcile` remains in `generations`, even after the scheduler removes it from all active structures. Repeated standing-root changes therefore retain historical key strings for the scheduler lifetime; use an active-only generation map with a separate monotonic epoch, or otherwise prune tombstones after stale completions are impossible.</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| /// are never removed by this owner. | ||
| owned_actors: Mutex<HashMap<String, (ProjectRootId, bool)>>, | ||
| schedule: Mutex<StandingScheduleState>, | ||
| semantic_cache: Arc<Mutex<HashMap<String, SemanticBuildCache>>>, |
There was a problem hiding this comment.
P2: When a semantic build is cancelled or its root is removed, this actor retains its file inventory and embedding model indefinitely. Prune semantic cache entries during reconciliation and cancellation, not only after successful completion.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/subc/standing.rs, line 105:
<comment>When a semantic build is cancelled or its root is removed, this actor retains its file inventory and embedding model indefinitely. Prune semantic cache entries during reconciliation and cancellation, not only after successful completion.</comment>
<file context>
@@ -90,6 +102,7 @@ pub(super) struct StandingActor {
/// are never removed by this owner.
owned_actors: Mutex<HashMap<String, (ProjectRootId, bool)>>,
schedule: Mutex<StandingScheduleState>,
+ semantic_cache: Arc<Mutex<HashMap<String, SemanticBuildCache>>>,
}
</file context>
Signed-off-by: Naadir Jeewa <naadir@randomvariable.co.uk> Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
|
@greptile-apps review |
There was a problem hiding this comment.
5 issues found across 14 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/aft/src/executor/tests.rs">
<violation number="1" location="crates/aft/src/executor/tests.rs:2768">
P2: After `remove_actor` sends `retirement_rx`, the scheduler may not have processed its wake yet, so this snapshot can still report one queued job and intermittently fail. Poll until the post-removal liveness publication observes zero before asserting the counts.</violation>
</file>
<file name="crates/aft/src/subc/standing.rs">
<violation number="1" location="crates/aft/src/subc/standing.rs:888">
P1: When a source file is added while a callgraph build spans multiple slices, this cache keeps the original inventory because it refreshes only when `resolved_target` changes. The resumable builder stages exactly `files`, so the new file is omitted from the published graph until a later full pass. Revalidate the corpus or invalidate this cache when the root contents change before reusing it.</violation>
</file>
<file name="crates/aft/src/thread_priority.rs">
<violation number="1" location="crates/aft/src/thread_priority.rs:80">
P3: The module doc for Linux still says 'Restores to SCHED_OTHER (nice 0)', but restore() now passes previous.scheduler and previous.scheduler_param, restoring the thread's exact pre-demotion policy and priority rather than forcing SCHED_OTHER. For the actual worker threads (SCHED_OTHER baseline) behavior is unchanged, so this is only a documentation inaccuracy introduced by the diff; update the comment to say it restores the previous policy/parameters (SCHED_OTHER/nice-0 in the common case).</violation>
</file>
<file name="crates/aft/src/search_index.rs">
<violation number="1" location="crates/aft/src/search_index.rs:961">
P2: When a staging manifest has `validation_cursor > files.len()`, the new guard rejects it only from `existing`; the later `compatible` predicate omits the same check. If its corpus fingerprint still matches, publication skips content validation and can publish stale spill postings; apply the invariant to the compatible predicate too.</violation>
<violation number="2" location="crates/aft/src/search_index.rs:1084">
P2: When a file changes after its validation window but preserves size and mtime, the sliced build never rehashes it before publication. The corpus fingerprint still matches, so stale postings are published; tie validation to a change generation or restart validation when any validated file can have changed.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| let mut cache = cache.lock(); | ||
| let needs_refresh = cache | ||
| .get(&entry.literal_path) | ||
| .is_none_or(|cached| cached.resolved_target != entry.resolved_target); |
There was a problem hiding this comment.
P1: When a source file is added while a callgraph build spans multiple slices, this cache keeps the original inventory because it refreshes only when resolved_target changes. The resumable builder stages exactly files, so the new file is omitted from the published graph until a later full pass. Revalidate the corpus or invalidate this cache when the root contents change before reusing it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/subc/standing.rs, line 888:
<comment>When a source file is added while a callgraph build spans multiple slices, this cache keeps the original inventory because it refreshes only when `resolved_target` changes. The resumable builder stages exactly `files`, so the new file is omitted from the published graph until a later full pass. Revalidate the corpus or invalidate this cache when the root contents change before reusing it.</comment>
<file context>
@@ -843,7 +881,24 @@ fn build_missing_callgraph_after_strict_check(
+ let mut cache = cache.lock();
+ let needs_refresh = cache
+ .get(&entry.literal_path)
+ .is_none_or(|cached| cached.resolved_target != entry.resolved_target);
+ if needs_refresh {
+ let files = crate::callgraph::walk_project_files(&entry.resolved_target)
</file context>
| && manifest.canonical_root == canonical_root | ||
| && manifest.cursor <= manifest.paths.len() | ||
| && manifest.files.len() == manifest.cursor | ||
| && manifest.validation_cursor <= manifest.files.len() |
There was a problem hiding this comment.
P2: When a staging manifest has validation_cursor > files.len(), the new guard rejects it only from existing; the later compatible predicate omits the same check. If its corpus fingerprint still matches, publication skips content validation and can publish stale spill postings; apply the invariant to the compatible predicate too.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/search_index.rs, line 961:
<comment>When a staging manifest has `validation_cursor > files.len()`, the new guard rejects it only from `existing`; the later `compatible` predicate omits the same check. If its corpus fingerprint still matches, publication skips content validation and can publish stale spill postings; apply the invariant to the compatible predicate too.</comment>
<file context>
@@ -957,6 +958,7 @@ impl SearchIndex {
&& manifest.canonical_root == canonical_root
&& manifest.cursor <= manifest.paths.len()
&& manifest.files.len() == manifest.cursor
+ && manifest.validation_cursor <= manifest.files.len()
});
// Fresh start (or structurally invalid staging): walk and fingerprint
</file context>
|
|
||
| // Publication validation is itself sliced. Re-hash only one bounded | ||
| // file window per turn so large roots cannot monopolize a cold slot. | ||
| if manifest.validation_cursor < manifest.files.len() { |
There was a problem hiding this comment.
P2: When a file changes after its validation window but preserves size and mtime, the sliced build never rehashes it before publication. The corpus fingerprint still matches, so stale postings are published; tie validation to a change generation or restart validation when any validated file can have changed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/search_index.rs, line 1084:
<comment>When a file changes after its validation window but preserves size and mtime, the sliced build never rehashes it before publication. The corpus fingerprint still matches, so stale postings are published; tie validation to a change generation or restart validation when any validated file can have changed.</comment>
<file context>
@@ -1076,10 +1079,19 @@ impl SearchIndex {
- // build when any content no longer matches its staged hash.
+ // Publication validation is itself sliced. Re-hash only one bounded
+ // file window per turn so large roots cannot monopolize a cold slot.
+ if manifest.validation_cursor < manifest.files.len() {
+ let end = (manifest.validation_cursor + slice_size).min(manifest.files.len());
+ if !staged_contents_match_disk(&manifest.files[manifest.validation_cursor..end]) {
</file context>
| "accounting-retirement-queued".to_string(), | ||
| Box::new(|_| ok("accounting-retirement-queued")), | ||
| ); | ||
| let snapshot = executor |
There was a problem hiding this comment.
P2: After remove_actor sends retirement_rx, the scheduler may not have processed its wake yet, so this snapshot can still report one queued job and intermittently fail. Poll until the post-removal liveness publication observes zero before asserting the counts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/executor/tests.rs, line 2768:
<comment>After `remove_actor` sends `retirement_rx`, the scheduler may not have processed its wake yet, so this snapshot can still report one queued job and intermittently fail. Poll until the post-removal liveness publication observes zero before asserting the counts.</comment>
<file context>
@@ -2746,16 +2759,30 @@ fn queue_accounting_tracks_dispatch_cancellation_and_actor_retirement() {
+ "accounting-retirement-queued".to_string(),
+ Box::new(|_| ok("accounting-retirement-queued")),
+ );
+ let snapshot = executor
+ .try_dispatch_liveness_snapshot()
+ .expect("liveness before actor retirement");
</file context>
| } | ||
|
|
||
| pub fn restore(previous: PreviousPriority) { | ||
| cpu_policy(previous.scheduler, previous.scheduler_param); |
There was a problem hiding this comment.
P3: The module doc for Linux still says 'Restores to SCHED_OTHER (nice 0)', but restore() now passes previous.scheduler and previous.scheduler_param, restoring the thread's exact pre-demotion policy and priority rather than forcing SCHED_OTHER. For the actual worker threads (SCHED_OTHER baseline) behavior is unchanged, so this is only a documentation inaccuracy introduced by the diff; update the comment to say it restores the previous policy/parameters (SCHED_OTHER/nice-0 in the common case).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/thread_priority.rs, line 80:
<comment>The module doc for Linux still says 'Restores to SCHED_OTHER (nice 0)', but restore() now passes previous.scheduler and previous.scheduler_param, restoring the thread's exact pre-demotion policy and priority rather than forcing SCHED_OTHER. For the actual worker threads (SCHED_OTHER baseline) behavior is unchanged, so this is only a documentation inaccuracy introduced by the diff; update the comment to say it restores the previous policy/parameters (SCHED_OTHER/nice-0 in the common case).</comment>
<file context>
@@ -75,7 +77,7 @@ mod imp {
pub fn restore(previous: PreviousPriority) {
- cpu_policy(previous.scheduler);
+ cpu_policy(previous.scheduler, previous.scheduler_param);
if !io_set(IOPRIO_WHO_PROCESS, tid(), previous.io_priority) {
warn_once("io", &std::io::Error::last_os_error().to_string());
</file context>
|
@cubic-dev-ai review commit 1654923 |
@randomvariable I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
5 existing issues remain and 18 new issues found across 67 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/aft/src/commands/configure.rs">
<violation number="1" location="crates/aft/src/commands/configure.rs:3416">
P1: When a configure worker and a standing slice publish the same artifact in this process, `acquire_shared` returns the existing lease instead of blocking, so the new lease does not prevent concurrent cache writes. Add a process-local publication lock shared by both session and standing paths, or make both paths hold the same cache lock across publication.</violation>
</file>
<file name="crates/aft/src/callgraph_store/mod.rs">
<violation number="1" location="crates/aft/src/callgraph_store/mod.rs:763">
P0: When a standing callgraph build starts, this budget check yields after inventory staging, but the next retry rebuilds that inventory and reaches the same boundary again. Persist a resumable inventory/extraction cursor or defer yielding until a durable resumable phase is established; otherwise every slice returns `Progress` and the root never publishes.</violation>
</file>
<file name="crates/aft/src/subc/bash.rs">
<violation number="1" location="crates/aft/src/subc/bash.rs:461">
P1: When an admitted poll finishes after the request deadline and returns `Promote`, this deadline rejects the follow-up promotion after the poll has already removed foreground bookkeeping. Submit promotion without the expired request deadline so the running command is always handed off to background.</violation>
</file>
<file name="crates/aft/src/subc/standing.rs">
<violation number="1" location="crates/aft/src/subc/standing.rs:509">
P2: While a session owns an artifact, `admit_build` returns `None`, but this response keeps the DRR item alive and causes a no-op maintenance job on every tick. Park the entry until unbind, or return `has_more: false` so reconciliation re-adds it after the session releases the artifact.</violation>
<violation number="2" location="crates/aft/src/subc/standing.rs:548">
P1: Every maintenance tick rebuilds completed semantic and callgraph artifacts because those kinds have no successful strict-verification path here. Gate these builders on an artifact freshness check, or retain and consume a durable successful-verification state before dispatching another build.</violation>
</file>
<file name="crates/aft/src/resource_policy.rs">
<violation number="1" location="crates/aft/src/resource_policy.rs:116">
P1: When Linux PSI reports any nonzero `avg10`, even `0.01%`, this marks the signal `High` and stops new standing slices; compare against a documented pressure threshold instead of zero.</violation>
</file>
<file name="packages/pi-plugin/src/tools/bash.ts">
<violation number="1" location="packages/pi-plugin/src/tools/bash.ts:528">
P2: When `wait:true` is used, `serverWait` is always sent as false. Rust therefore does not detach the wait when a new user message arrives, contrary to the tool contract; propagate detach intent separately from the completion-blocking flag.</violation>
</file>
<file name="crates/aft/src/standing_scheduler.rs">
<violation number="1" location="crates/aft/src/standing_scheduler.rs:37">
P2: When a configured root is removed after its slice completes, this reconciliation drops its deficit but leaves its generation tombstone indefinitely. Retain generations only for active or in-flight roots so repeated root churn cannot grow the map without bound.</violation>
</file>
<file name="crates/aft/src/cold_build_limiter.rs">
<violation number="1" location="crates/aft/src/cold_build_limiter.rs:172">
P1: When lifecycle admission is revoked after `admit_build` but before this call, the permit is still granted. The standing slice can then clear the bind's `needs_strict_verify` marker; preserve the cancellation check before strict verification or recording.</violation>
<violation number="2" location="crates/aft/src/cold_build_limiter.rs:172">
P2: A non-standing waiter can register after the pre-check but before this call, allowing standing work to take the slot ahead of it. Recheck `has_non_standing_waiters()` in the post-acquisition callback.</violation>
</file>
<file name="crates/aft/src/executor/tests.rs">
<violation number="1" location="crates/aft/src/executor/tests.rs:2658">
P2: This test does not cover the dedupe-capacity path despite its name and comments. Fill the queue to `MAINTENANCE_QUEUE_CAP`, submit the duplicate, then assert that a replacement is admitted and the removed duplicate receives the expected response.</violation>
<violation number="2" location="crates/aft/src/executor/tests.rs:2760">
P2: After `remove_actor`, this assertion can observe the pre-removal liveness snapshot because scheduler publication is asynchronous. Poll until the scheduler publishes the zero depth, or provide a synchronized retirement completion before asserting the snapshot.</violation>
<violation number="3" location="crates/aft/src/executor/tests.rs:2847">
P2: This test is timing-sensitive: a 20 ms deadline can expire before the scheduler dispatches the job, causing the start wait to fail instead of testing dispatched-deadline behavior. Use a generous deadline and keep the job running past it after dispatch is confirmed.</violation>
</file>
<file name="crates/aft/src/subc/mod.rs">
<violation number="1" location="crates/aft/src/subc/mod.rs:4593">
P1: When a RouteBind waits in the executor queue past 12 seconds, this deadline produces `request_deadline_exceeded`, but the bind error mapper reports it as `config_divergence`. Preserve a transient deadline/queue error such as `actor_not_ready` so clients retry instead of treating the root as permanently divergent.</violation>
</file>
<file name="crates/aft/src/search_index.rs">
<violation number="1" location="crates/aft/src/search_index.rs:975">
P1: On a large standing root, the initial and publication turns perform an unbounded recursive walk and metadata scan before yielding, defeating the promised bounded search slices. Make corpus discovery and fingerprinting resumable or reuse incrementally persisted discovery state.</violation>
<violation number="2" location="crates/aft/src/search_index.rs:1025">
P1: On large roots, each staged path rescans the entire accumulated manifest to allocate its file ID, turning indexing into quadratic work. Maintain the next included ID as a running counter instead of recounting `manifest.files` for every path.</violation>
<violation number="3" location="crates/aft/src/search_index.rs:1060">
P1: When a path is unreadable during one slice but readable before publication, this staging record omits it without revalidation, so later searches never see the recovered file. Store skipped paths as included/unindexed so the fallback search can discover them.</violation>
<violation number="4" location="crates/aft/src/search_index.rs:1078">
P1: Every 32-file slice rewrites and fsyncs the entire growing manifest, causing quadratic checkpoint I/O on large roots. Persist the immutable path list separately and checkpoint only incremental state or append-only staging metadata.</violation>
</file>
Requires human review: Auto-approval blocked by 7 unresolved issues from previous reviews.
Re-trigger cubic
| _ => false, | ||
| }); | ||
| if exhausted { | ||
| return Err(CallGraphStoreError::SliceProgress { |
There was a problem hiding this comment.
P0: When a standing callgraph build starts, this budget check yields after inventory staging, but the next retry rebuilds that inventory and reaches the same boundary again. Persist a resumable inventory/extraction cursor or defer yielding until a durable resumable phase is established; otherwise every slice returns Progress and the root never publishes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/callgraph_store/mod.rs, line 763:
<comment>When a standing callgraph build starts, this budget check yields after inventory staging, but the next retry rebuilds that inventory and reaches the same boundary again. Persist a resumable inventory/extraction cursor or defer yielding until a durable resumable phase is established; otherwise every slice returns `Progress` and the root never publishes.</comment>
<file context>
@@ -724,16 +742,31 @@ pub(crate) fn with_publish_epoch<R>(
+ _ => false,
+ });
+ if exhausted {
+ return Err(CallGraphStoreError::SliceProgress {
+ phase: stage.to_string(),
+ completed,
</file context>
| &root_for_search, | ||
| ); | ||
| let published = match search_lease { | ||
| Ok(Some(_lease)) => { |
There was a problem hiding this comment.
P1: When a configure worker and a standing slice publish the same artifact in this process, acquire_shared returns the existing lease instead of blocking, so the new lease does not prevent concurrent cache writes. Add a process-local publication lock shared by both session and standing paths, or make both paths hold the same cache lock across publication.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/commands/configure.rs, line 3416:
<comment>When a configure worker and a standing slice publish the same artifact in this process, `acquire_shared` returns the existing lease instead of blocking, so the new lease does not prevent concurrent cache writes. Add a process-local publication lock shared by both session and standing paths, or make both paths hold the same cache lock across publication.</comment>
<file context>
@@ -3403,11 +3404,35 @@ fn schedule_artifact_loads(
+ &root_for_search,
+ );
+ let published = match search_lease {
+ Ok(Some(_lease)) => {
+ search_persist_epoch_flag.run_if_current(search_persist_epoch, || {
+ let head = index.stored_git_head().map(str::to_owned);
</file context>
| response | ||
| }) | ||
| }), | ||
| request_deadline, |
There was a problem hiding this comment.
P1: When an admitted poll finishes after the request deadline and returns Promote, this deadline rejects the follow-up promotion after the poll has already removed foreground bookkeeping. Submit promotion without the expired request deadline so the running command is always handed off to background.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/subc/bash.rs, line 461:
<comment>When an admitted poll finishes after the request deadline and returns `Promote`, this deadline rejects the follow-up promotion after the poll has already removed foreground bookkeeping. Submit promotion without the expired request deadline so the running command is always handed off to background.</comment>
<file context>
@@ -421,6 +458,7 @@ pub(super) fn submit_deferred_bash(
response
})
}),
+ request_deadline,
);
</file context>
| request_deadline, | |
| None, |
| _permit: permit, | ||
| admission_epoch, | ||
| } | ||
| try_acquire_classified_with_limiter(limiter, &request).map(|permit| StandingColdBuildPermit { |
There was a problem hiding this comment.
P1: When lifecycle admission is revoked after admit_build but before this call, the permit is still granted. The standing slice can then clear the bind's needs_strict_verify marker; preserve the cancellation check before strict verification or recording.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/cold_build_limiter.rs, line 172:
<comment>When lifecycle admission is revoked after `admit_build` but before this call, the permit is still granted. The standing slice can then clear the bind's `needs_strict_verify` marker; preserve the cancellation check before strict verification or recording.</comment>
<file context>
@@ -153,23 +153,25 @@ pub(crate) struct StandingColdBuildPermit {
- _permit: permit,
- admission_epoch,
- }
+ try_acquire_classified_with_limiter(limiter, &request).map(|permit| StandingColdBuildPermit {
+ _permit: permit,
+ admission_epoch,
</file context>
| return crate::protocol::Response::success( | ||
| response_request_id, | ||
| serde_json::json!({"standing": true, "entry": literal_path, "admitted": false}), | ||
| serde_json::json!({"standing": true, "entry": literal_path, "admitted": false, "has_more": true}), |
There was a problem hiding this comment.
P2: While a session owns an artifact, admit_build returns None, but this response keeps the DRR item alive and causes a no-op maintenance job on every tick. Park the entry until unbind, or return has_more: false so reconciliation re-adds it after the session releases the artifact.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/subc/standing.rs, line 509:
<comment>While a session owns an artifact, `admit_build` returns `None`, but this response keeps the DRR item alive and causes a no-op maintenance job on every tick. Park the entry until unbind, or return `has_more: false` so reconciliation re-adds it after the session releases the artifact.</comment>
<file context>
@@ -184,86 +474,122 @@ impl StandingActor {
return crate::protocol::Response::success(
response_request_id,
- serde_json::json!({"standing": true, "entry": literal_path, "admitted": false}),
+ serde_json::json!({"standing": true, "entry": literal_path, "admitted": false, "has_more": true}),
);
};
</file context>
| serde_json::json!({"standing": true, "entry": literal_path, "admitted": false, "has_more": true}), | |
| serde_json::json!({"standing": true, "entry": literal_path, "admitted": false, "has_more": false}), |
Signed-off-by: Naadir Jeewa <naadir@randomvariable.co.uk> Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
|
@cubic-dev-ai review commit 4988711 |
@randomvariable I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
6 existing issues remain and 12 new issues found across 68 files
You’re at about 92% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/aft/src/semantic_index.rs">
<violation number="1" location="crates/aft/src/semantic_index.rs:2070">
P1: When a staged file changes without changing its size or reported mtime, `resume_cold_build_slice` accepts stale chunks and publishes embeddings for the old source. Include content hashes in the resume identity, or revalidate each staged file before embedding and publication.</violation>
<violation number="2" location="crates/aft/src/semantic_index.rs:3095">
P2: For a large standing root, the final semantic slice still reads and serializes the entire index in one maintenance job, defeating the bounded-slice responsiveness goal. Make materialization/publication resumable or enforce deadline checks while processing the staged corpus.</violation>
</file>
<file name="packages/pi-plugin/src/tools/_shared.ts">
<violation number="1" location="packages/pi-plugin/src/tools/_shared.ts:55">
P2: Direct `callBridge` commands never receive this execution deadline because `bridge.send` only serializes request parameters; its options affect transport timing, not server execution. Propagate `deadline_ms_remaining` for raw commands as well, or route these calls through `toolCall`, so timed-out work does not continue consuming the AFT process.</violation>
</file>
<file name="crates/aft/src/search_index.rs">
<violation number="1" location="crates/aft/src/search_index.rs:1158">
P1: When validation completes, this call merges every staged posting and writes the entire index in one turn, so publication can block standing maintenance for a large root. Split publication into resumable bounded steps or make the merge deadline-aware.</violation>
</file>
<file name="crates/aft/src/checkpoint.rs">
<violation number="1" location="crates/aft/src/checkpoint.rs:965">
P2: When a scope has a missing or corrupt `.last-used` marker, this fallback treats it as recently used, so crashed or pre-marker scopes are retained forever and the scope store can accumulate orphan directories. Treat an unknown marker as stale (the subsequent per-scope lock acquisition already protects active owners).</violation>
</file>
<file name="crates/aft/src/executor/mod.rs">
<violation number="1" location="crates/aft/src/executor/mod.rs:2904">
P2: Interactive `HeavyInit` jobs are demoted to background priority here, so callgraph/lazy-initialization requests can be starved under load despite the separate interactive lane. Apply background priority only when `run_job.job_class == JobClass::Maintenance`, while keeping interactive HeavyInit work at normal priority.</violation>
</file>
<file name="packages/aft-bridge/src/subc-transport.ts">
<violation number="1" location="packages/aft-bridge/src/subc-transport.ts:819">
P2: When a caller supplies `NaN` or `Infinity`, this condition puts an invalid deadline on the wire. Guard the value with `Number.isFinite` before assigning `deadline_ms_remaining`, matching `BinaryBridge.toolCall`.</violation>
</file>
<file name="crates/aft/tests/integration/subc_storm_test.rs">
<violation number="1" location="crates/aft/tests/integration/subc_storm_test.rs:1737">
P2: This test does not exercise standing-root maintenance: it submits generic maintenance jobs and only mirrors the production cold-admission code in a test closure. A production standing pass could block or fail to yield while this regression test still passes; drive the actual standing-root scheduling path instead of duplicating its behavior in the harness.</violation>
</file>
<file name="crates/aft/src/subc/standing.rs">
<violation number="1" location="crates/aft/src/subc/standing.rs:277">
P1: When a standing literal path resolves to a different target without changing its index selection, the scheduler keeps the old generation and accepts stale slice results. Reset the generation whenever the full entry identity changes, not only when `indexes` changes.</violation>
<violation number="2" location="crates/aft/src/subc/standing.rs:277">
P1: When `resolved_target` or `artifact_key` changes without changing `indexes`, the in-flight slice keeps the old generation and its completion is accepted for the replacement root. Include those identity fields in this change check so reconciliation reconfigures the generation and ignores stale completion state.</violation>
<violation number="3" location="crates/aft/src/subc/standing.rs:889">
P2: On a large standing root, the first callgraph slice performs an unbounded full-tree walk before the bounded builder starts. Move discovery into the resumable staged inventory or process it in bounded batches so one maintenance job cannot monopolize the executor.</violation>
</file>
<file name="packages/pi-plugin/src/__tests__/config.test.ts">
<violation number="1" location="packages/pi-plugin/src/__tests__/config.test.ts:81">
P3: A project config setting index.resource_policy is silently dropped with no warning, unlike every comparable user-only field (gh_read, backup, subc) which emits an "Ignoring ... from project config" warning that its test asserts on stderr. Add index.resource_policy to getStrippedTopLevelKeys and assert the warning here so users learn their repo-level resource_policy (e.g. forcing resource_policy:"performance") was ignored.</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 6 unresolved issues already reported by Cubic.
Re-trigger cubic
| hasher.update(path.to_string_lossy().as_bytes()); | ||
| if let Ok(metadata) = fs::metadata(path) { | ||
| hasher.update(&metadata.len().to_le_bytes()); | ||
| let modified = metadata |
There was a problem hiding this comment.
P1: When a staged file changes without changing its size or reported mtime, resume_cold_build_slice accepts stale chunks and publishes embeddings for the old source. Include content hashes in the resume identity, or revalidate each staged file before embedding and publication.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/semantic_index.rs, line 2070:
<comment>When a staged file changes without changing its size or reported mtime, `resume_cold_build_slice` accepts stale chunks and publishes embeddings for the old source. Include content hashes in the resume identity, or revalidate each staged file before embedding and publication.</comment>
<file context>
@@ -2020,6 +2055,161 @@ fn borrowed_artifact_identity(data_path: &Path) -> Result<(String, blake3::Hash)
+ hasher.update(path.to_string_lossy().as_bytes());
+ if let Ok(metadata) = fs::metadata(path) {
+ hasher.update(&metadata.len().to_le_bytes());
+ let modified = metadata
+ .modified()
+ .ok()
</file context>
| .into_iter() | ||
| .map(|source| Box::new(source) as Box<dyn PostingRecordSource>) | ||
| .collect(); | ||
| let base = write_cache_file_from_sources(cache_dir, &plan, &mut sources)?; |
There was a problem hiding this comment.
P1: When validation completes, this call merges every staged posting and writes the entire index in one turn, so publication can block standing maintenance for a large root. Split publication into resumable bounded steps or make the merge deadline-aware.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/search_index.rs, line 1158:
<comment>When validation completes, this call merges every staged posting and writes the entire index in one turn, so publication can block standing maintenance for a large root. Split publication into resumable bounded steps or make the merge deadline-aware.</comment>
<file context>
@@ -900,6 +935,231 @@ impl SearchIndex {
+ .into_iter()
+ .map(|source| Box::new(source) as Box<dyn PostingRecordSource>)
+ .collect();
+ let base = write_cache_file_from_sources(cache_dir, &plan, &mut sources)?;
+ drop(base);
+ fs::remove_dir_all(&staging_dir)?;
</file context>
| return Ok(SemanticBuildSliceOutcome::Yielded); | ||
| } | ||
|
|
||
| let chunks = read_semantic_chunks(&chunks_path, &manifest.chunk_offsets)?; |
There was a problem hiding this comment.
P2: For a large standing root, the final semantic slice still reads and serializes the entire index in one maintenance job, defeating the bounded-slice responsiveness goal. Make materialization/publication resumable or enforce deadline checks while processing the staged corpus.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/semantic_index.rs, line 3095:
<comment>For a large standing root, the final semantic slice still reads and serializes the entire index in one maintenance job, defeating the bounded-slice responsiveness goal. Make materialization/publication resumable or enforce deadline checks while processing the staged corpus.</comment>
<file context>
@@ -2778,6 +2968,192 @@ impl SemanticIndex {
+ return Ok(SemanticBuildSliceOutcome::Yielded);
+ }
+
+ let chunks = read_semantic_chunks(&chunks_path, &manifest.chunk_offsets)?;
+ let vectors =
+ read_semantic_vectors(&vectors_path, manifest.vectors_count, fingerprint.dimension)?;
</file context>
| let used_at = fs::read_to_string(&marker) | ||
| .ok() | ||
| .and_then(|value| value.parse::<u64>().ok()) | ||
| .unwrap_or(now); |
There was a problem hiding this comment.
P2: When a scope has a missing or corrupt .last-used marker, this fallback treats it as recently used, so crashed or pre-marker scopes are retained forever and the scope store can accumulate orphan directories. Treat an unknown marker as stale (the subsequent per-scope lock acquisition already protects active owners).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/checkpoint.rs, line 965:
<comment>When a scope has a missing or corrupt `.last-used` marker, this fallback treats it as recently used, so crashed or pre-marker scopes are retained forever and the scope store can accumulate orphan directories. Treat an unknown marker as stale (the subsequent per-scope lock acquisition already protects active owners).</comment>
<file context>
@@ -936,6 +946,36 @@ impl CheckpointStore {
+ let used_at = fs::read_to_string(&marker)
+ .ok()
+ .and_then(|value| value.parse::<u64>().ok())
+ .unwrap_or(now);
+ if now.saturating_sub(used_at) < NAMED_CHECKPOINT_RETENTION_SECS {
+ continue;
</file context>
| .unwrap_or(now); | |
| .unwrap_or_default(); |
| // configure and user-initiated tool mutations. Only cold builds | ||
| // (HeavyInit) and background subsystem drains (MaintenanceCommit) | ||
| // are deferrable maintenance work. | ||
| Lane::HeavyInit | Lane::MaintenanceCommit => { |
There was a problem hiding this comment.
P2: Interactive HeavyInit jobs are demoted to background priority here, so callgraph/lazy-initialization requests can be starved under load despite the separate interactive lane. Apply background priority only when run_job.job_class == JobClass::Maintenance, while keeping interactive HeavyInit work at normal priority.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/src/executor/mod.rs, line 2904:
<comment>Interactive `HeavyInit` jobs are demoted to background priority here, so callgraph/lazy-initialization requests can be starved under load despite the separate interactive lane. Apply background priority only when `run_job.job_class == JobClass::Maintenance`, while keeping interactive HeavyInit work at normal priority.</comment>
<file context>
@@ -2347,6 +2891,22 @@ fn worker_loop(run_rx: Receiver<RunJob>, event_tx: Sender<SchedulerEvent>) {
+ // configure and user-initiated tool mutations. Only cold builds
+ // (HeavyInit) and background subsystem drains (MaintenanceCommit)
+ // are deferrable maintenance work.
+ Lane::HeavyInit | Lane::MaintenanceCommit => {
+ crate::thread_priority::with_background(|| run_lane_job(run_job))
+ }
</file context>
| Lane::HeavyInit | Lane::MaintenanceCommit => { | |
| Lane::HeavyInit | Lane::MaintenanceCommit | |
| if run_job.job_class == JobClass::Maintenance => { |
| const editSlotSurvives = this.pool.getEditSlotSurvives(); | ||
| if (editSlotSurvives !== undefined) body.edit_slot_survives = editSlotSurvives; | ||
| if (preview === true) body.preview = true; | ||
| if (executionDeadlineMs !== undefined) body.deadline_ms_remaining = executionDeadlineMs; |
There was a problem hiding this comment.
P2: When a caller supplies NaN or Infinity, this condition puts an invalid deadline on the wire. Guard the value with Number.isFinite before assigning deadline_ms_remaining, matching BinaryBridge.toolCall.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/aft-bridge/src/subc-transport.ts, line 819:
<comment>When a caller supplies `NaN` or `Infinity`, this condition puts an invalid deadline on the wire. Guard the value with `Number.isFinite` before assigning `deadline_ms_remaining`, matching `BinaryBridge.toolCall`.</comment>
<file context>
@@ -803,11 +811,12 @@ class SubcTransport implements AftProjectTransport {
const editSlotSurvives = this.pool.getEditSlotSurvives();
if (editSlotSurvives !== undefined) body.edit_slot_survives = editSlotSurvives;
if (preview === true) body.preview = true;
+ if (executionDeadlineMs !== undefined) body.deadline_ms_remaining = executionDeadlineMs;
const reply = await this.pool.routeRequest(
this.identityFor(sessionId),
</file context>
| if (executionDeadlineMs !== undefined) body.deadline_ms_remaining = executionDeadlineMs; | |
| if (Number.isFinite(executionDeadlineMs)) body.deadline_ms_remaining = executionDeadlineMs; |
| let yielded_probe = Arc::clone(&yielded); | ||
| let request_id = format!("storm-standing-yield-{index}"); | ||
| receivers.push(( | ||
| executor.submit_maintenance_async( |
There was a problem hiding this comment.
P2: This test does not exercise standing-root maintenance: it submits generic maintenance jobs and only mirrors the production cold-admission code in a test closure. A production standing pass could block or fail to yield while this regression test still passes; drive the actual standing-root scheduling path instead of duplicating its behavior in the harness.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/aft/tests/integration/subc_storm_test.rs, line 1737:
<comment>This test does not exercise standing-root maintenance: it submits generic maintenance jobs and only mirrors the production cold-admission code in a test closure. A production standing pass could block or fail to yield while this regression test still passes; drive the actual standing-root scheduling path instead of duplicating its behavior in the harness.</comment>
<file context>
@@ -1668,6 +1689,152 @@ async fn drive_heavy_init_saturation_daemon(input: FakeDaemonInput) {
+ let yielded_probe = Arc::clone(&yielded);
+ let request_id = format!("storm-standing-yield-{index}");
+ receivers.push((
+ executor.submit_maintenance_async(
+ root_id,
+ Lane::MaintenanceCommit,
</file context>
| ); | ||
|
|
||
| const fixture = createConfigFixture(); | ||
| writeFileSync( |
There was a problem hiding this comment.
P3: A project config setting index.resource_policy is silently dropped with no warning, unlike every comparable user-only field (gh_read, backup, subc) which emits an "Ignoring ... from project config" warning that its test asserts on stderr. Add index.resource_policy to getStrippedTopLevelKeys and assert the warning here so users learn their repo-level resource_policy (e.g. forcing resource_policy:"performance") was ignored.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/pi-plugin/src/__tests__/config.test.ts, line 81:
<comment>A project config setting index.resource_policy is silently dropped with no warning, unlike every comparable user-only field (gh_read, backup, subc) which emits an "Ignoring ... from project config" warning that its test asserts on stderr. Add index.resource_policy to getStrippedTopLevelKeys and assert the warning here so users learn their repo-level resource_policy (e.g. forcing resource_policy:"performance") was ignored.</comment>
<file context>
@@ -66,6 +66,33 @@ afterEach(() => {
+ );
+
+ const fixture = createConfigFixture();
+ writeFileSync(
+ fixture.userConfigPath,
+ JSON.stringify({ index: { resource_policy: "performance" } }),
</file context>
Signed-off-by: Naadir Jeewa <naadir@randomvariable.co.uk> Co-authored-by: alfonso-aft <289616620+alfonso-aft@users.noreply.github.com>
|
Hmm, I think this needs a bigger rethink. |
Summary
index.resource_policy = "performance"bypasses resource admission while preserving concurrency and correctness boundsTest Plan
cargo fmt --all -- --checkcargo test -p agent-file-tools standing_rootscargo test -p agent-file-tools callgraph_storecargo test -p agent-file-tools search_indexcargo test -p agent-file-tools semantic_indexbun test packages/opencode-plugin/src/__tests__/config.test.ts packages/pi-plugin/src/__tests__/config.test.tsgit diff --checkContribution
AI-assisted implementation and verification with OpenAI Codex through Oh My Pi. The author reviewed the changes and test evidence.
Greptile Summary
The PR restructures standing-root indexing into bounded, resumable work while carrying request deadlines and protecting interactive responsiveness.
Confidence Score: 5/5
The PR appears safe to merge because the previously reported standing-root cursor and stale-completion failures are addressed and no blocking failure remains.
No blocking failure remains.
Important Files Changed
Sequence Diagram
sequenceDiagram participant Tick as Standing maintenance tick participant Roots as Standing roots participant DRR as Deficit-round-robin scheduler participant Exec as Bounded executor participant Build as Resumable index builder Tick->>Roots: Reconcile changed configuration Roots-->>Tick: Active root entries Tick->>DRR: Reconcile identities and generations Tick->>DRR: Request next admitted root DRR-->>Tick: Root and generation Tick->>Exec: Submit one index-kind slice Exec->>Build: Resume durable staging work Build-->>Exec: Complete or yield Exec-->>Tick: Result with has_more and kind_complete Tick->>DRR: Validate generation and charge elapsed cost alt Current completion has more work DRR->>DRR: Requeue root fairly else Stale completion DRR->>DRR: Fence former generation endReviews (6): Last reviewed commit: "fix(index): reset replaced root generati..." | Re-trigger Greptile