feat(wake): add auto-wake Manager scheduler with 3 selectable strategies
New wake system for Manager agents: persistent (resume prompt), on-demand (fresh session), smart (threshold-gated). 6 wake signals from tribunal S-3 hybrid. Pure scorer function (24 tests), Svelte 5 rune scheduler store, SettingsTab UI (strategy button + threshold slider), AgentSession integration.
This commit is contained in:
parent
5576392d4b
commit
c774f352ee
9 changed files with 891 additions and 2 deletions
|
|
@ -22,6 +22,7 @@
|
|||
import type { AgentMessage } from '../../adapters/claude-messages';
|
||||
import { getProvider, getDefaultProviderId } from '../../providers/registry.svelte';
|
||||
import { loadAnchorsForProject } from '../../stores/anchors.svelte';
|
||||
import { getWakeEvent, consumeWakeEvent, updateManagerSession } from '../../stores/wake-scheduler.svelte';
|
||||
import { SessionId, ProjectId } from '../../types/ids';
|
||||
import AgentPane from '../Agent/AgentPane.svelte';
|
||||
|
||||
|
|
@ -87,6 +88,46 @@
|
|||
startReinjectionTimer();
|
||||
onDestroy(() => {
|
||||
if (reinjectionTimer) clearInterval(reinjectionTimer);
|
||||
if (wakeCheckTimer) clearInterval(wakeCheckTimer);
|
||||
});
|
||||
|
||||
// Wake scheduler integration — poll for wake events (Manager agents only)
|
||||
let wakeCheckTimer: ReturnType<typeof setInterval> | null = null;
|
||||
const isManager = $derived(project.isAgent && project.agentRole === 'manager');
|
||||
|
||||
function startWakeCheck() {
|
||||
if (wakeCheckTimer) clearInterval(wakeCheckTimer);
|
||||
if (!isManager) return;
|
||||
wakeCheckTimer = setInterval(() => {
|
||||
if (contextRefreshPrompt) return; // Don't queue if already has a pending prompt
|
||||
const event = getWakeEvent(project.id);
|
||||
if (!event) return;
|
||||
|
||||
if (event.mode === 'fresh') {
|
||||
// On-demand / Smart: reset session, inject wake context as initial prompt
|
||||
handleNewSession();
|
||||
contextRefreshPrompt = buildWakePrompt(event.context.evaluation.summary);
|
||||
} else {
|
||||
// Persistent: resume existing session with wake context
|
||||
contextRefreshPrompt = buildWakePrompt(event.context.evaluation.summary);
|
||||
}
|
||||
|
||||
consumeWakeEvent(project.id);
|
||||
}, 5_000); // Check every 5s
|
||||
}
|
||||
|
||||
function buildWakePrompt(summary: string): string {
|
||||
return `[Auto-Wake] You have been woken by the auto-wake scheduler. Here is the current fleet status:\n\n${summary}\n\nCheck your inbox with \`btmsg inbox\` and review the task board with \`bttask board\`. Take action on any urgent items above.`;
|
||||
}
|
||||
|
||||
// Start wake check when component mounts (for managers)
|
||||
$effect(() => {
|
||||
if (isManager) {
|
||||
startWakeCheck();
|
||||
} else if (wakeCheckTimer) {
|
||||
clearInterval(wakeCheckTimer);
|
||||
wakeCheckTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
let sessionId = $state(SessionId(crypto.randomUUID()));
|
||||
|
|
@ -102,6 +143,8 @@
|
|||
contextRefreshPrompt = undefined;
|
||||
registerSessionProject(sessionId, ProjectId(project.id), providerId);
|
||||
trackProject(ProjectId(project.id), sessionId);
|
||||
// Notify wake scheduler of new session ID
|
||||
if (isManager) updateManagerSession(project.id, sessionId);
|
||||
onsessionid?.(sessionId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@
|
|||
import { getProjectHealth, setStallThreshold } from '../../stores/health.svelte';
|
||||
import { fsWatchProject, fsUnwatchProject, onFsWriteDetected, fsWatcherStatus } from '../../adapters/fs-watcher-bridge';
|
||||
import { recordExternalWrite } from '../../stores/conflicts.svelte';
|
||||
import { ProjectId } from '../../types/ids';
|
||||
import { ProjectId, type AgentId, type GroupId } from '../../types/ids';
|
||||
import { notify, dismissNotification } from '../../stores/notifications.svelte';
|
||||
import { registerManager, unregisterManager, updateManagerConfig } from '../../stores/wake-scheduler.svelte';
|
||||
|
||||
interface Props {
|
||||
project: ProjectConfig;
|
||||
|
|
@ -66,6 +67,32 @@
|
|||
setStallThreshold(project.id, project.stallThresholdMin ?? null);
|
||||
});
|
||||
|
||||
// Register Manager agents with the wake scheduler
|
||||
$effect(() => {
|
||||
if (!(project.isAgent && project.agentRole === 'manager')) return;
|
||||
const groupId = activeGroup?.id;
|
||||
if (!groupId || !mainSessionId) return;
|
||||
|
||||
// Find the agent config to get wake settings
|
||||
const agentConfig = activeGroup?.agents?.find(a => a.id === project.id);
|
||||
const strategy = agentConfig?.wakeStrategy ?? 'smart';
|
||||
const intervalMin = agentConfig?.wakeIntervalMin ?? 3;
|
||||
const threshold = agentConfig?.wakeThreshold ?? 0.5;
|
||||
|
||||
registerManager(
|
||||
project.id as unknown as AgentId,
|
||||
groupId as unknown as GroupId,
|
||||
mainSessionId,
|
||||
strategy,
|
||||
intervalMin,
|
||||
threshold,
|
||||
);
|
||||
|
||||
return () => {
|
||||
unregisterManager(project.id);
|
||||
};
|
||||
});
|
||||
|
||||
// S-1 Phase 2: start filesystem watcher for this project's CWD
|
||||
$effect(() => {
|
||||
const cwd = project.cwd;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
import { getProviders } from '../../providers/registry.svelte';
|
||||
import type { ProviderId, ProviderSettings } from '../../providers/types';
|
||||
import { ANCHOR_BUDGET_SCALES, ANCHOR_BUDGET_SCALE_LABELS, type AnchorBudgetScale } from '../../types/anchors';
|
||||
import { WAKE_STRATEGIES, WAKE_STRATEGY_LABELS, WAKE_STRATEGY_DESCRIPTIONS, type WakeStrategy } from '../../types/wake';
|
||||
|
||||
const PROJECT_ICONS = [
|
||||
'📁', '🚀', '🤖', '🌐', '🔧', '🎮', '📱', '💻',
|
||||
|
|
@ -704,6 +705,45 @@
|
|||
<span class="scale-label">{agent.wakeIntervalMin ?? 3} min</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-field">
|
||||
<span class="card-field-label">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
||||
Wake Strategy
|
||||
</span>
|
||||
<div class="wake-strategy-row">
|
||||
{#each WAKE_STRATEGIES as strat}
|
||||
<button
|
||||
class="strategy-btn"
|
||||
class:active={(agent.wakeStrategy ?? 'smart') === strat}
|
||||
title={WAKE_STRATEGY_DESCRIPTIONS[strat]}
|
||||
onclick={() => updateAgent(activeGroupId, agent.id, { wakeStrategy: strat })}
|
||||
>{WAKE_STRATEGY_LABELS[strat]}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<span class="setting-hint">{WAKE_STRATEGY_DESCRIPTIONS[agent.wakeStrategy ?? 'smart']}</span>
|
||||
</div>
|
||||
|
||||
{#if (agent.wakeStrategy ?? 'smart') === 'smart'}
|
||||
<div class="card-field">
|
||||
<span class="card-field-label">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg>
|
||||
Wake Threshold
|
||||
</span>
|
||||
<div class="scale-slider">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={agent.wakeThreshold ?? 0.5}
|
||||
oninput={e => updateAgent(activeGroupId, agent.id, { wakeThreshold: parseFloat((e.target as HTMLInputElement).value) })}
|
||||
/>
|
||||
<span class="scale-label">{((agent.wakeThreshold ?? 0.5) * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<span class="setting-hint">Only wakes when signal score exceeds this level</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="card-field">
|
||||
|
|
@ -1428,6 +1468,41 @@
|
|||
min-width: 5.5em;
|
||||
}
|
||||
|
||||
.wake-strategy-row {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
}
|
||||
|
||||
.strategy-btn {
|
||||
flex: 1;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: none;
|
||||
background: var(--ctp-surface0);
|
||||
color: var(--ctp-overlay1);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
}
|
||||
|
||||
.strategy-btn:not(:last-child) {
|
||||
border-right: 1px solid var(--ctp-surface1);
|
||||
}
|
||||
|
||||
.strategy-btn:hover {
|
||||
background: var(--ctp-surface1);
|
||||
color: var(--ctp-subtext1);
|
||||
}
|
||||
|
||||
.strategy-btn.active {
|
||||
background: color-mix(in srgb, var(--ctp-blue) 20%, var(--ctp-surface0));
|
||||
color: var(--ctp-blue);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* CWD input: left-ellipsis */
|
||||
.cwd-input {
|
||||
direction: rtl;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue