arbiter-ai 1.0.3 → 1.0.5

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.
Binary file
package/dist/index.js CHANGED
@@ -1,10 +1,49 @@
1
1
  #!/usr/bin/env node
2
2
  // Main entry point for the Arbiter system
3
3
  // Ties together state, router, and TUI for the hierarchical AI orchestration system
4
+ import fs from 'node:fs';
4
5
  import { Router } from './router.js';
5
6
  import { loadSession } from './session-persistence.js';
6
7
  import { createInitialState } from './state.js';
7
8
  import { checkGitignore, createTUI, showCharacterSelect, showForestIntro, showTitleScreen, } from './tui/index.js';
9
+ import { getAllSprites } from './tui/animation-loop.js';
10
+ import { TILE } from './tui/tileset.js';
11
+ /**
12
+ * Get package.json version
13
+ */
14
+ function getVersion() {
15
+ const pkgPath = new URL('../package.json', import.meta.url);
16
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
17
+ return pkg.version;
18
+ }
19
+ /**
20
+ * Print help message and exit
21
+ */
22
+ function printHelp() {
23
+ console.log(`
24
+ arbiter - Hierarchical AI orchestration system
25
+
26
+ Consult with the Arbiter, a wise overseer who commands a
27
+ council of Orchestrators to tackle complex tasks. Each layer
28
+ extends Claude's context, keeping the work on track. Bring
29
+ a detailed markdown description of your requirements.
30
+
31
+ USAGE
32
+ arbiter [options] [requirements-file]
33
+
34
+ OPTIONS
35
+ -h, --help Show this help message
36
+ -v, --version Show version number
37
+ --resume Resume from saved session (if <24h old)
38
+ --demo-animations Run animation demo (skip intro screens and router)
39
+
40
+ EXAMPLES
41
+ arbiter Start fresh session
42
+ arbiter ./SPEC.md Start with requirements file (skip in-game prompt)
43
+ arbiter --resume Resume previous session
44
+ arbiter --demo-animations Run animation demo
45
+ `);
46
+ }
8
47
  /**
9
48
  * Outputs session information to stderr for resume capability
10
49
  * Format per architecture doc:
@@ -19,6 +58,89 @@ function outputSessionInfo(state) {
19
58
  // Output to stderr so it doesn't interfere with TUI output
20
59
  process.stderr.write(`${JSON.stringify(sessionInfo)}\n`);
21
60
  }
61
+ /**
62
+ * Helper function to create a delay promise
63
+ */
64
+ function delay(ms) {
65
+ return new Promise((resolve) => setTimeout(resolve, ms));
66
+ }
67
+ /**
68
+ * Runs a demo sequence showing all animations
69
+ * Used when --demo-animations flag is passed
70
+ */
71
+ async function runDemoSequence() {
72
+ // Wait for TUI to fully initialize and sprites to be registered
73
+ await delay(1500);
74
+ // Get all registered sprites
75
+ const sprites = getAllSprites();
76
+ const human = sprites.find((s) => s.id === 'human');
77
+ const arbiter = sprites.find((s) => s.id === 'arbiter');
78
+ const scroll = sprites.find((s) => s.id === 'scroll');
79
+ const spellbook = sprites.find((s) => s.id === 'spellbook');
80
+ const smoke = sprites.find((s) => s.id === 'smoke');
81
+ const demons = sprites.filter((s) => s.id.startsWith('demon-'));
82
+ if (!human || !arbiter || !scroll || !spellbook || !smoke) {
83
+ process.stderr.write('Demo: Failed to find required sprites\n');
84
+ process.exit(1);
85
+ }
86
+ // Demo entrance sequence - human walks in
87
+ await human.walk({ row: 2, col: 1 });
88
+ await delay(300);
89
+ // Human hops (surprised)
90
+ await human.hop(2);
91
+ await delay(300);
92
+ // Arbiter hops (notices visitor)
93
+ await arbiter.hop(2);
94
+ await delay(500);
95
+ // Demo scroll drop
96
+ await scroll.physicalSpawn();
97
+ await delay(500);
98
+ // Demo arbiter walks to scroll
99
+ await arbiter.walk({ row: 2, col: 3 });
100
+ await delay(300);
101
+ // Arbiter notices scroll (alarmed)
102
+ await arbiter.alarmed(1500);
103
+ await delay(500);
104
+ // Demo summon sequence - arbiter walks to fire for summoning
105
+ await arbiter.walk({ row: 3, col: 4 });
106
+ await delay(300);
107
+ // Spellbook appears
108
+ await spellbook.physicalSpawn();
109
+ await delay(500);
110
+ // Demo cauldron bubbling
111
+ smoke.startBubbling();
112
+ await delay(1000);
113
+ // Spawn demons one by one with magic effect
114
+ for (let i = 0; i < Math.min(3, demons.length); i++) {
115
+ await demons[i].magicSpawn();
116
+ await delay(500);
117
+ }
118
+ await delay(1500);
119
+ // Demo hopping while working
120
+ await arbiter.hop(4);
121
+ await delay(1000);
122
+ // Demo dismiss sequence
123
+ for (const demon of demons.filter((d) => d.visible)) {
124
+ await demon.magicDespawn();
125
+ await delay(300);
126
+ }
127
+ // Stop bubbling
128
+ smoke.stopBubbling();
129
+ await delay(500);
130
+ // Hide spellbook
131
+ spellbook.visible = false;
132
+ await delay(500);
133
+ // Walk back
134
+ await arbiter.walk({ row: 2, col: 3 });
135
+ await delay(500);
136
+ // Demo chat indicator
137
+ await arbiter.chatting(2000);
138
+ await delay(500);
139
+ // Demo complete - wait a moment then exit
140
+ await delay(2000);
141
+ // Exit cleanly
142
+ process.exit(0);
143
+ }
22
144
  /**
23
145
  * Main application entry point
24
146
  * Creates and wires together all components of the Arbiter system
@@ -64,6 +186,44 @@ async function main() {
64
186
  try {
65
187
  // Parse CLI arguments
66
188
  const args = process.argv.slice(2);
189
+ // Handle --help flag (early exit)
190
+ if (args.includes('--help') || args.includes('-h')) {
191
+ printHelp();
192
+ process.exit(0);
193
+ }
194
+ // Handle --version flag (early exit)
195
+ if (args.includes('--version') || args.includes('-v')) {
196
+ console.log(getVersion());
197
+ process.exit(0);
198
+ }
199
+ // Handle --demo-animations flag (early exit)
200
+ const demoMode = args.includes('--demo-animations');
201
+ if (demoMode) {
202
+ // Demo mode: skip all intro screens and router initialization
203
+ // Go straight to main TUI with default character and run scripted animation demo
204
+ state = createInitialState();
205
+ // Set a requirements path to skip the requirements overlay prompt
206
+ state.requirementsPath = '/dev/null';
207
+ // Create TUI with default character
208
+ tui = createTUI(state, TILE.HUMAN_1);
209
+ // Wire TUI exit to shutdown
210
+ tui.onExit(() => {
211
+ shutdown(0);
212
+ });
213
+ // Start TUI (takes over terminal)
214
+ tui.start();
215
+ // Fire-and-forget the requirements ready callback (skips requirement selection)
216
+ tui.onRequirementsReady(() => {
217
+ // No-op since we're not starting the router
218
+ });
219
+ // Run demo sequence after TUI initializes
220
+ runDemoSequence();
221
+ // Keep the process running until demo completes
222
+ await new Promise(() => {
223
+ // This promise never resolves - demo will exit via process.exit(0)
224
+ });
225
+ return;
226
+ }
67
227
  const shouldResume = args.includes('--resume');
68
228
  // Handle --resume flag
69
229
  let savedSession = null;
@@ -74,7 +234,7 @@ async function main() {
74
234
  }
75
235
  }
76
236
  // Check for positional requirements file argument (first non-flag arg)
77
- const positionalArgs = args.filter((arg) => !arg.startsWith('--'));
237
+ const positionalArgs = args.filter((arg) => !arg.startsWith('--') && !arg.startsWith('-'));
78
238
  const cliRequirementsFile = positionalArgs[0] || null;
79
239
  let selectedCharacter;
80
240
  if (savedSession) {
package/dist/router.d.ts CHANGED
@@ -63,6 +63,7 @@ export declare class Router {
63
63
  private watchdogInterval;
64
64
  private arbiterMcpServer;
65
65
  private arbiterHooks;
66
+ private arbiterPollContext;
66
67
  private contextPollInterval;
67
68
  private crashCount;
68
69
  constructor(state: AppState, callbacks: RouterCallbacks);
@@ -77,7 +78,7 @@ export declare class Router {
77
78
  private startContextPolling;
78
79
  /**
79
80
  * Poll context for all active sessions
80
- * Forks sessions and runs /context to get accurate values
81
+ * Uses paired pollers that share the same options as the sessions
81
82
  */
82
83
  private pollAllContexts;
83
84
  /**
@@ -127,17 +128,15 @@ export declare class Router {
127
128
  private createOrchestratorOptions;
128
129
  /**
129
130
  * Creates and starts the Arbiter session with MCP tools
131
+ * @param resumeSessionId - Optional session ID for resuming an existing session
130
132
  */
131
133
  private startArbiterSession;
132
134
  /**
133
135
  * Creates and starts an Orchestrator session
136
+ * @param number - The orchestrator number (I, II, III...)
137
+ * @param resumeSessionId - Optional session ID for resuming an existing session
134
138
  */
135
139
  private startOrchestratorSession;
136
- /**
137
- * Resume an existing Orchestrator session
138
- * Similar to startOrchestratorSession but uses resume option and skips introduction
139
- */
140
- private resumeOrchestratorSession;
141
140
  /**
142
141
  * Send a message to the Arbiter
143
142
  */
package/dist/router.js CHANGED
@@ -11,6 +11,49 @@ function sleep(ms) {
11
11
  // Retry constants for crash recovery
12
12
  const MAX_RETRIES = 3;
13
13
  const RETRY_DELAYS = [1000, 2000, 4000]; // 1s, 2s, 4s exponential backoff
14
+ /**
15
+ * Creates a query and a paired context poller that uses the same options.
16
+ * This ensures context polling always matches the session's actual configuration.
17
+ *
18
+ * @param prompt - Initial prompt for the session
19
+ * @param options - Options used to create the session (will also be used for polling)
20
+ * @returns Tuple of [query generator, context polling function]
21
+ */
22
+ function createQueryWithPoller(prompt, options) {
23
+ const q = query({ prompt, options });
24
+ const pollContext = async (sessionId) => {
25
+ try {
26
+ const pollQuery = query({
27
+ prompt: '/context',
28
+ options: {
29
+ ...options,
30
+ resume: sessionId,
31
+ forkSession: true, // Fork to avoid polluting main session
32
+ },
33
+ });
34
+ let percent = null;
35
+ for await (const msg of pollQuery) {
36
+ // /context output comes through as user message with the token info
37
+ if (msg.type === 'user') {
38
+ const content = msg.message?.content;
39
+ if (typeof content === 'string') {
40
+ // Match: **Tokens:** 18.4k / 200.0k (9%)
41
+ const match = content.match(/\*\*Tokens:\*\*\s*([0-9.]+)k\s*\/\s*200\.?0?k\s*\((\d+)%\)/i);
42
+ if (match) {
43
+ percent = parseInt(match[2], 10);
44
+ }
45
+ }
46
+ }
47
+ }
48
+ return percent;
49
+ }
50
+ catch (_error) {
51
+ // Silently fail - context polling is best-effort
52
+ return null;
53
+ }
54
+ };
55
+ return [q, pollContext];
56
+ }
14
57
  // Arbiter's allowed tools: MCP tools + read-only exploration
15
58
  const ARBITER_ALLOWED_TOOLS = [
16
59
  'mcp__arbiter-tools__spawn_orchestrator',
@@ -55,45 +98,6 @@ import { createOrchestratorHooks, ORCHESTRATOR_SYSTEM_PROMPT, } from './orchestr
55
98
  const MAX_CONTEXT_TOKENS = 200000;
56
99
  // Context polling interval (1 minute)
57
100
  const CONTEXT_POLL_INTERVAL_MS = 60_000;
58
- /**
59
- * Poll context usage by forking a session and running /context
60
- * Uses forkSession: true to avoid polluting the main conversation
61
- * Note: This does clutter resume history - no workaround found yet
62
- *
63
- * @param sessionId - The session ID to fork and check
64
- * @returns Context percentage (0-100) or null if polling failed
65
- */
66
- async function pollContextForSession(sessionId) {
67
- try {
68
- const q = query({
69
- prompt: '/context',
70
- options: {
71
- resume: sessionId,
72
- forkSession: true, // Fork to avoid polluting main session
73
- permissionMode: 'bypassPermissions',
74
- },
75
- });
76
- let percent = null;
77
- for await (const msg of q) {
78
- // /context output comes through as user message with the token info
79
- if (msg.type === 'user') {
80
- const content = msg.message?.content;
81
- if (typeof content === 'string') {
82
- // Match: **Tokens:** 18.4k / 200.0k (9%)
83
- const match = content.match(/\*\*Tokens:\*\*\s*([0-9.]+)k\s*\/\s*200\.?0?k\s*\((\d+)%\)/i);
84
- if (match) {
85
- percent = parseInt(match[2], 10);
86
- }
87
- }
88
- }
89
- }
90
- return percent;
91
- }
92
- catch (_error) {
93
- // Silently fail - context polling is best-effort
94
- return null;
95
- }
96
- }
97
101
  /**
98
102
  * Formats an SDK message for debug logging
99
103
  * Returns a human-readable string representation of the message
@@ -263,6 +267,8 @@ export class Router {
263
267
  arbiterMcpServer = null;
264
268
  // Store Arbiter hooks for session resumption
265
269
  arbiterHooks = null;
270
+ // Paired context poller for Arbiter (uses same options as session)
271
+ arbiterPollContext = null;
266
272
  // Context polling timer - polls /context once per minute via session forking
267
273
  contextPollInterval = null;
268
274
  // Track crash recovery attempts for TUI display
@@ -295,12 +301,12 @@ export class Router {
295
301
  }
296
302
  /**
297
303
  * Poll context for all active sessions
298
- * Forks sessions and runs /context to get accurate values
304
+ * Uses paired pollers that share the same options as the sessions
299
305
  */
300
306
  async pollAllContexts() {
301
- // Poll Arbiter context
302
- if (this.state.arbiterSessionId) {
303
- const arbiterPercent = await pollContextForSession(this.state.arbiterSessionId);
307
+ // Poll Arbiter context using paired poller
308
+ if (this.state.arbiterSessionId && this.arbiterPollContext) {
309
+ const arbiterPercent = await this.arbiterPollContext(this.state.arbiterSessionId);
304
310
  if (arbiterPercent !== null) {
305
311
  updateArbiterContext(this.state, arbiterPercent);
306
312
  this.callbacks.onDebugLog?.({
@@ -310,10 +316,11 @@ export class Router {
310
316
  });
311
317
  }
312
318
  }
313
- // Poll Orchestrator context if active
319
+ // Poll Orchestrator context using paired poller
314
320
  let orchPercent = null;
315
- if (this.currentOrchestratorSession?.sessionId) {
316
- orchPercent = await pollContextForSession(this.currentOrchestratorSession.sessionId);
321
+ const orchSession = this.currentOrchestratorSession;
322
+ if (orchSession?.sessionId && orchSession.pollContext) {
323
+ orchPercent = await orchSession.pollContext(orchSession.sessionId);
317
324
  if (orchPercent !== null) {
318
325
  updateOrchestratorContext(this.state, orchPercent);
319
326
  this.callbacks.onDebugLog?.({
@@ -330,13 +337,11 @@ export class Router {
330
337
  * Resume from a previously saved session
331
338
  */
332
339
  async resumeFromSavedSession(saved) {
333
- // Set arbiter session ID so startArbiterSession uses resume
334
- this.state.arbiterSessionId = saved.arbiterSessionId;
335
- // Start arbiter (it will use the session ID for resume)
336
- await this.startArbiterSession();
340
+ // Start arbiter with resume session ID
341
+ await this.startArbiterSession(saved.arbiterSessionId);
337
342
  // If there was an active orchestrator, resume it too
338
343
  if (saved.orchestratorSessionId && saved.orchestratorNumber) {
339
- await this.resumeOrchestratorSession(saved.orchestratorSessionId, saved.orchestratorNumber);
344
+ await this.startOrchestratorSession(saved.orchestratorNumber, saved.orchestratorSessionId);
340
345
  }
341
346
  // Start context polling
342
347
  this.startContextPolling();
@@ -488,6 +493,7 @@ export class Router {
488
493
  hooks: this.arbiterHooks ?? undefined,
489
494
  abortController: this.arbiterAbortController ?? new AbortController(),
490
495
  permissionMode: 'bypassPermissions',
496
+ settingSources: ['project'], // Load CLAUDE.md for project context
491
497
  allowedTools: [...ARBITER_ALLOWED_TOOLS],
492
498
  ...(resumeSessionId ? { resume: resumeSessionId } : {}),
493
499
  };
@@ -505,6 +511,7 @@ export class Router {
505
511
  hooks,
506
512
  abortController,
507
513
  permissionMode: 'bypassPermissions',
514
+ settingSources: ['project'], // Load CLAUDE.md for project context
508
515
  allowedTools: [...ORCHESTRATOR_ALLOWED_TOOLS],
509
516
  outputFormat: {
510
517
  type: 'json_schema',
@@ -515,8 +522,9 @@ export class Router {
515
522
  }
516
523
  /**
517
524
  * Creates and starts the Arbiter session with MCP tools
525
+ * @param resumeSessionId - Optional session ID for resuming an existing session
518
526
  */
519
- async startArbiterSession() {
527
+ async startArbiterSession(resumeSessionId) {
520
528
  // Notify that we're waiting for Arbiter
521
529
  this.callbacks.onWaitingStart?.('arbiter');
522
530
  // Create abort controller for this session
@@ -554,12 +562,12 @@ export class Router {
554
562
  const hooks = createArbiterHooks(arbiterHooksCallbacks);
555
563
  this.arbiterHooks = hooks;
556
564
  // Create options using helper
557
- const options = this.createArbiterOptions(this.state.arbiterSessionId ?? undefined);
558
- // Note: The Arbiter session runs continuously.
559
- // We'll send messages to it and process responses in a loop.
560
- // Create initial prompt - reference requirements file if provided
561
- const initialPrompt = this.state.requirementsPath
562
- ? `@${this.state.requirementsPath}
565
+ const options = this.createArbiterOptions(resumeSessionId);
566
+ // Choose prompt based on whether resuming or starting fresh
567
+ const prompt = resumeSessionId
568
+ ? '[System: Session resumed. Continue where you left off.]'
569
+ : this.state.requirementsPath
570
+ ? `@${this.state.requirementsPath}
563
571
 
564
572
  A Scroll of Requirements has been presented.
565
573
 
@@ -578,23 +586,28 @@ Your task now is to achieve COMPLETE UNDERSTANDING before any work begins. This
578
586
  Only when we have achieved 100% alignment on vision, scope, and approach - only when you could explain this task to an Orchestrator with complete confidence - only then do we proceed.
579
587
 
580
588
  Take your time. This phase determines everything that follows.`
581
- : 'Speak, mortal.';
582
- this.arbiterQuery = query({
583
- prompt: initialPrompt,
584
- options,
585
- });
589
+ : 'Speak, mortal.';
590
+ const [arbiterQuery, pollContext] = createQueryWithPoller(prompt, options);
591
+ this.arbiterQuery = arbiterQuery;
592
+ this.arbiterPollContext = pollContext;
593
+ // Store session ID if resuming (will be updated from init message if new)
594
+ if (resumeSessionId) {
595
+ this.state.arbiterSessionId = resumeSessionId;
596
+ }
586
597
  // Process the initial response
587
598
  await this.processArbiterMessages(this.arbiterQuery);
588
599
  }
589
600
  /**
590
601
  * Creates and starts an Orchestrator session
602
+ * @param number - The orchestrator number (I, II, III...)
603
+ * @param resumeSessionId - Optional session ID for resuming an existing session
591
604
  */
592
- async startOrchestratorSession(number) {
593
- // Clean up any existing orchestrator before spawning new one (hard abort)
605
+ async startOrchestratorSession(number, resumeSessionId) {
606
+ // Clean up any existing orchestrator before spawning/resuming
594
607
  this.cleanupOrchestrator();
595
608
  // Notify that we're waiting for Orchestrator
596
609
  this.callbacks.onWaitingStart?.('orchestrator');
597
- // Increment orchestrator count
610
+ // Set orchestrator count
598
611
  this.orchestratorCount = number;
599
612
  // Generate unique ID for this orchestrator
600
613
  const orchId = `orch-${Date.now()}`;
@@ -630,113 +643,31 @@ Take your time. This phase determines everything that follows.`
630
643
  const hooks = createOrchestratorHooks(orchestratorCallbacks,
631
644
  // Context percent getter - returns state value (updated by polling)
632
645
  (_sessionId) => this.state.currentOrchestrator?.contextPercent || 0);
633
- // Create options using helper (no resume for initial session)
634
- const options = this.createOrchestratorOptions(hooks, abortController);
635
- // Create the orchestrator query
636
- const orchestratorQuery = query({
637
- prompt: 'Introduce yourself and await instructions from the Arbiter.',
638
- options,
639
- });
640
- // Create the full OrchestratorSession object
641
- this.currentOrchestratorSession = {
642
- id: orchId,
643
- number,
644
- sessionId: '', // Will be set when we get the init message
645
- query: orchestratorQuery,
646
- abortController,
647
- toolCallCount: 0,
648
- queue: [],
649
- lastActivityTime: Date.now(),
650
- hooks,
651
- };
652
- // Set up TUI-facing orchestrator state before processing
653
- // We'll update the session ID when we get the init message
654
- setCurrentOrchestrator(this.state, {
655
- id: orchId,
656
- sessionId: '', // Will be set when we get the init message
657
- number,
658
- });
659
- // Switch mode
660
- setMode(this.state, 'arbiter_to_orchestrator');
661
- this.callbacks.onModeChange('arbiter_to_orchestrator');
662
- // Notify about orchestrator spawn (for tile scene demon spawning)
663
- this.callbacks.onOrchestratorSpawn?.(number);
664
- // Update context display to show orchestrator (initially at 0%)
665
- this.callbacks.onContextUpdate(this.state.arbiterContextPercent, this.state.currentOrchestrator?.contextPercent ?? null);
666
- // Start watchdog timer
667
- this.startWatchdog();
668
- // Process orchestrator messages
669
- await this.processOrchestratorMessages(this.currentOrchestratorSession.query);
670
- }
671
- /**
672
- * Resume an existing Orchestrator session
673
- * Similar to startOrchestratorSession but uses resume option and skips introduction
674
- */
675
- async resumeOrchestratorSession(sessionId, number) {
676
- // Clean up any existing orchestrator before resuming
677
- this.cleanupOrchestrator();
678
- // Notify that we're waiting for Orchestrator
679
- this.callbacks.onWaitingStart?.('orchestrator');
680
- // Restore orchestrator count
681
- this.orchestratorCount = number;
682
- // Generate unique ID for this orchestrator
683
- const orchId = `orch-${Date.now()}`;
684
- // Create abort controller for this session
685
- const abortController = new AbortController();
686
- // Create callbacks for hooks
687
- const orchestratorCallbacks = {
688
- onContextUpdate: (_sessionId, _percent) => {
689
- // Context is now tracked via periodic polling (pollAllContexts)
690
- // This callback exists for hook compatibility but is unused
691
- },
692
- onToolUse: (tool) => {
693
- // Increment tool count on the session
694
- if (this.currentOrchestratorSession) {
695
- this.currentOrchestratorSession.toolCallCount++;
696
- const newCount = this.currentOrchestratorSession.toolCallCount;
697
- // Update state and notify callback
698
- updateOrchestratorTool(this.state, tool, newCount);
699
- this.callbacks.onToolUse(tool, newCount);
700
- // Log tool use to debug (logbook) with orchestrator context
701
- const conjuringLabel = `Conjuring ${toRoman(number)}`;
702
- this.callbacks.onDebugLog?.({
703
- type: 'tool',
704
- speaker: conjuringLabel,
705
- text: `[Tool] ${tool}`,
706
- details: { tool, count: newCount },
707
- });
708
- }
709
- },
710
- };
711
- // Create hooks for tool use tracking
712
- // Context is now tracked via polling, not hooks
713
- const hooks = createOrchestratorHooks(orchestratorCallbacks,
714
- // Context percent getter - returns state value (updated by polling)
715
- (_sessionId) => this.state.currentOrchestrator?.contextPercent || 0);
716
- // Create options using helper with resume
717
- const options = this.createOrchestratorOptions(hooks, abortController, sessionId);
718
- // Create the orchestrator query with a continuation prompt (not introduction)
719
- const orchestratorQuery = query({
720
- prompt: '[System: Session resumed. Continue where you left off.]',
721
- options,
722
- });
646
+ // Create options using helper
647
+ const options = this.createOrchestratorOptions(hooks, abortController, resumeSessionId);
648
+ // Choose prompt based on whether resuming or starting fresh
649
+ const prompt = resumeSessionId
650
+ ? '[System: Session resumed. Continue where you left off.]'
651
+ : 'Introduce yourself and await instructions from the Arbiter.';
652
+ // Create the orchestrator query with paired context poller
653
+ const [orchestratorQuery, pollContext] = createQueryWithPoller(prompt, options);
723
654
  // Create the full OrchestratorSession object
724
- // Note: sessionId is already known from the saved session
725
655
  this.currentOrchestratorSession = {
726
656
  id: orchId,
727
657
  number,
728
- sessionId: sessionId, // Already known, don't set to empty string
658
+ sessionId: resumeSessionId ?? '', // Known if resuming, will be set from init message if new
729
659
  query: orchestratorQuery,
730
660
  abortController,
731
661
  toolCallCount: 0,
732
662
  queue: [],
733
663
  lastActivityTime: Date.now(),
734
664
  hooks,
665
+ pollContext,
735
666
  };
736
667
  // Set up TUI-facing orchestrator state
737
668
  setCurrentOrchestrator(this.state, {
738
669
  id: orchId,
739
- sessionId: sessionId,
670
+ sessionId: resumeSessionId ?? '',
740
671
  number,
741
672
  });
742
673
  // Switch mode
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Animation loop module for managing sprite animations
3
+ *
4
+ * This module provides a centralized animation tick system that manages
5
+ * a collection of sprites and updates them at ~60fps. It tracks actual
6
+ * delta time between ticks for smooth animations regardless of system load.
7
+ */
8
+ import { Sprite } from './sprite.js';
9
+ /**
10
+ * Register a sprite with the animation loop
11
+ *
12
+ * Once registered, the sprite's tick() method will be called
13
+ * on each animation frame with the elapsed time delta.
14
+ *
15
+ * @param sprite - The sprite to register
16
+ */
17
+ export declare function registerSprite(sprite: Sprite): void;
18
+ /**
19
+ * Unregister a sprite from the animation loop
20
+ *
21
+ * The sprite will no longer receive tick updates.
22
+ *
23
+ * @param id - The unique ID of the sprite to unregister
24
+ */
25
+ export declare function unregisterSprite(id: string): void;
26
+ /**
27
+ * Get a sprite by ID
28
+ *
29
+ * @param id - The unique ID of the sprite to retrieve
30
+ * @returns The sprite if found, or undefined if not registered
31
+ */
32
+ export declare function getSprite(id: string): Sprite | undefined;
33
+ /**
34
+ * Get all registered sprites
35
+ *
36
+ * @returns An array of all currently registered sprites
37
+ */
38
+ export declare function getAllSprites(): Sprite[];
39
+ /**
40
+ * Start the animation loop
41
+ *
42
+ * Runs at ~60fps (16ms interval) but tracks actual delta time
43
+ * for smooth animations. If the loop is already running, this
44
+ * function has no effect.
45
+ */
46
+ export declare function startAnimationLoop(): void;
47
+ /**
48
+ * Stop the animation loop
49
+ *
50
+ * Halts the animation tick. Sprites remain registered and can
51
+ * be resumed by calling startAnimationLoop() again.
52
+ */
53
+ export declare function stopAnimationLoop(): void;
54
+ /**
55
+ * Check if animation loop is running
56
+ *
57
+ * @returns true if the animation loop is currently active
58
+ */
59
+ export declare function isAnimationLoopRunning(): boolean;
60
+ /**
61
+ * Check if any registered sprite has an active animation
62
+ *
63
+ * @returns true if at least one sprite has a non-null animation
64
+ */
65
+ export declare function hasActiveAnimations(): boolean;