instar 0.6.15 → 0.7.1

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.
@@ -24,43 +24,68 @@ This wizard runs in a terminal that may be narrow (80-120 chars). Long text gets
24
24
  **Good** (fits in terminal):
25
25
  > Everything here is just a starting point. You can change any of it later — or just tell your agent to adjust itself.
26
26
 
27
- ## Phase 1: Welcome & Use Case Selection
27
+ ## Phase 1: Context Detection & Welcome
28
28
 
29
- Start with a brief welcome, then immediately ask HOW they want to use Instar.
29
+ **Do NOT ask "how do you want to use Instar?"** Instead, detect the context automatically and present an intelligent default.
30
+
31
+ ### Step 1a: Detect Environment
32
+
33
+ Run these checks BEFORE showing anything to the user:
34
+
35
+ ```bash
36
+ # Check if we're inside a git repository
37
+ git rev-parse --show-toplevel 2>/dev/null
38
+
39
+ # Get the repo name if it exists
40
+ basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null
41
+
42
+ # Check for common project indicators
43
+ ls package.json Cargo.toml pyproject.toml go.mod Gemfile pom.xml 2>/dev/null
44
+ ```
45
+
46
+ ### Step 1b: Present Context-Aware Welcome
47
+
48
+ **If inside a git repository:**
30
49
 
31
50
  ---
32
51
 
33
52
  **Welcome to Instar!**
34
53
 
35
- Instar turns Claude Code into a persistent agent you just talk to through Telegram, not the terminal. This setup gets you there. Two ways to use it:
54
+ I see you're in **[repo-name]**I'll set up a persistent agent for this project.
55
+
56
+ Your agent will monitor, build, and maintain this codebase. You'll talk to it through Telegram — no terminal needed after setup.
57
+
58
+ ---
59
+
60
+ Then proceed directly — no "project vs general" question needed. The context made it obvious.
61
+
62
+ If the user objects ("actually I want a personal agent, not a project agent"), accommodate immediately: "Got it — setting up a personal agent instead."
63
+
64
+ **If NOT inside a git repository:**
36
65
 
37
66
  ---
38
67
 
39
- Present a question with two clear options:
68
+ **Welcome to Instar!**
69
+
70
+ You're not inside a project, so I'll set up a personal agent — a persistent AI companion you talk to through Telegram.
71
+
72
+ It can research, schedule tasks, manage files, and grow over time.
73
+
74
+ ---
40
75
 
41
- 1. **Project Agent** Add an agent to an existing codebase. It monitors, builds, and maintains your project.
42
- 2. **General Agent** — A personal agent on your computer. Like having a persistent AI assistant you talk to through Telegram.
76
+ Then ask: "What should your agent be called?" (default: "my-agent")
43
77
 
44
- This choice determines the defaults, but the agent can always grow into more.
78
+ ### Key principle: Telegram is the interface, always
45
79
 
46
- ### If "Project Agent" selected:
47
- - Use the current working directory as the project
48
- - Default jobs focus on health checks, code monitoring, reflection
49
- - The agent's identity centers on the project
80
+ Regardless of project or personal agent, **Telegram is how you talk to your agent**. This should be clear from the very first message. Don't present it as an optional add-on — it's the destination of this entire setup.
50
81
 
51
- ### If "General Agent" selected:
52
- - Ask for a name for the agent (this becomes the directory name)
53
- - Create the directory in the current location or home dir
54
- - Default jobs focus on communication, scheduling, research
55
- - **Telegram is essential** — without it, a general agent has no natural interface
56
- - Frame the identity around being a personal assistant, not a code monitor
57
- - The AGENT.md should emphasize: "I'm your personal agent. Talk to me through Telegram."
82
+ The terminal session is the on-ramp. Telegram is where the agent experience lives.
58
83
 
59
84
  ## Phase 2: Identity Bootstrap — The Birth Conversation
60
85
 
61
86
  **This is the most important part.** Have a conversation to understand who the user is and who their agent will become. Keep it natural and concise.
62
87
 
63
- For **General Agents**: emphasize that this agent will be their persistent companion. It grows, learns, and communicates through Telegram. It's not a project tool — it's a presence.
88
+ For **Personal Agents**: emphasize that this agent will be their persistent companion. It grows, learns, and communicates through Telegram. It's not a project tool — it's a presence.
64
89
 
65
90
  For **Project Agents**: emphasize that this agent will own the project's health and development. It monitors, builds, and maintains.
66
91
 
@@ -225,30 +250,11 @@ This project uses instar for persistent agent capabilities.
225
250
  - **Research before escalating** — Check tools first. Build solutions. "Needs human" is last resort.
226
251
  ```
227
252
 
228
- ## Phase 3: Technical Configuration
229
-
230
- Now that identity is established, move to the technical setup. This feels more natural — the user already knows what they're building and why.
231
-
232
- ### 3a. Project Detection
233
-
234
- - The project directory is passed in the prompt (e.g., "The project to set up is at: /path/to/project")
235
- - All files should be written there, not in the instar package directory
236
- - Check if `.instar/config.json` already exists (offer to reconfigure or skip)
237
- - Verify prerequisites: check that `tmux` and `claude` CLI are available
238
-
239
- ```bash
240
- which tmux
241
- which claude
242
- ```
243
-
244
- ### 3b. Server Configuration
253
+ ## Phase 3: Telegram Setup — The Destination
245
254
 
246
- - **Port** (default: 4040) "The agent runs a small HTTP server for health checks and internal communication."
247
- - **Max sessions** (default: 3) — "This limits how many Claude sessions can run at once. 2-3 is usually right."
255
+ **Telegram comes BEFORE technical configuration.** It's the whole point everything else supports getting the user onto Telegram.
248
256
 
249
- ### 3c. Telegram Setup — The Destination
250
-
251
- **This terminal session is the on-ramp. Telegram is where the agent experience truly begins.** Frame it that way:
257
+ Frame it clearly:
252
258
 
253
259
  > Right now we're in a terminal. Telegram is where your agent comes alive:
254
260
  > - **Just talk** — no commands, no terminal, just conversation
@@ -256,9 +262,7 @@ which claude
256
262
  > - **Mobile access** — your agent is always reachable
257
263
  > - **Proactive** — your agent reaches out when something matters
258
264
 
259
- The goal of this setup is to get the user onto Telegram as fast as possible. Everything else (jobs, config, technical setup) supports that destination.
260
-
261
- For **General Agents**: Telegram is essential. Without it, there IS no natural interface. Be direct: "This is how you'll talk to your agent."
265
+ For **Personal Agents**: Telegram is essential. Without it, there IS no natural interface. Be direct: "This is how you'll talk to your agent."
262
266
 
263
267
  For **Project Agents**: Telegram is strongly recommended. Frame it as: "Your agent can message you about builds, issues, and progress — you just reply."
264
268
 
@@ -382,7 +386,32 @@ curl -s "https://api.telegram.org/bot${TOKEN}/getUpdates?timeout=5"
382
386
  - Look for `chat.id` where `chat.type` is "supergroup" or "group"
383
387
  - If auto-detection fails, guide manual entry
384
388
 
385
- ### 3d. Job Scheduler (Optional)
389
+ ## Phase 4: Technical Configuration
390
+
391
+ Now that identity and Telegram are established, handle the remaining technical setup. These should feel like sensible defaults, not interrogation.
392
+
393
+ ### 4a. Project Detection
394
+
395
+ - The project directory is passed in the prompt (e.g., "The project to set up is at: /path/to/project")
396
+ - All files should be written there, not in the instar package directory
397
+ - Check if `.instar/config.json` already exists (offer to reconfigure or skip)
398
+ - Verify prerequisites: check that `tmux` and `claude` CLI are available
399
+
400
+ ```bash
401
+ which tmux
402
+ which claude
403
+ ```
404
+
405
+ ### 4b. Server Configuration
406
+
407
+ Present sensible defaults — don't make the user think about these unless they want to:
408
+
409
+ - **Port** (default: 4040) — "The agent runs a small local server."
410
+ - **Max sessions** (default: 3) — "How many Claude sessions can run at once."
411
+
412
+ Ask as a single confirmation: "I'll use port 4040 with up to 3 sessions. Want to change these?" If yes, ask for specifics. If no, move on.
413
+
414
+ ### 4c. Job Scheduler (Optional)
386
415
 
387
416
  - Ask if they want scheduled jobs
388
417
  - If yes, walk through adding a first job:
@@ -393,7 +422,7 @@ curl -s "https://api.telegram.org/bot${TOKEN}/getUpdates?timeout=5"
393
422
  - **Execution type**: prompt (AI instruction), script (shell script), or skill (slash command)
394
423
  - Offer to add more jobs
395
424
 
396
- ### 3e. Write Configuration Files
425
+ ### 4d. Write Configuration Files
397
426
 
398
427
  Create the directory structure and write config files:
399
428
 
@@ -438,7 +467,7 @@ mkdir -p .instar/state/sessions .instar/state/jobs .instar/logs
438
467
 
439
468
  **`.instar/users.json`**: Array of user objects from the identity conversation.
440
469
 
441
- ### 3f. Update .gitignore
470
+ ### 4e. Update .gitignore
442
471
 
443
472
  Append if not present:
444
473
  ```
@@ -447,7 +476,7 @@ Append if not present:
447
476
  .instar/logs/
448
477
  ```
449
478
 
450
- ## Phase 4: Summary & Launch
479
+ ## Phase 5: Summary & Launch
451
480
 
452
481
  Show what was created briefly, then get the user to their agent.
453
482
 
@@ -485,4 +514,4 @@ Offer to start the server.
485
514
 
486
515
  ## Starting
487
516
 
488
- Begin by reading the project directory, checking for existing config, and then launching into the welcome explanation followed by the identity conversation. Let the conversation flow naturally.
517
+ Begin by detecting the environment (git repo check, project file check), then present the context-aware welcome. Let the conversation flow naturally from there.
package/dist/cli.js CHANGED
File without changes
@@ -28,6 +28,7 @@ import { TelegraphService } from '../publishing/TelegraphService.js';
28
28
  import { PrivateViewer } from '../publishing/PrivateViewer.js';
29
29
  import { TunnelManager } from '../tunnel/TunnelManager.js';
30
30
  import { EvolutionManager } from '../core/EvolutionManager.js';
31
+ import { QuotaTracker } from '../monitoring/QuotaTracker.js';
31
32
  /**
32
33
  * Respawn a session for a topic, including thread history in the bootstrap.
33
34
  * This prevents "thread drift" where respawned sessions lose context.
@@ -381,9 +382,24 @@ export async function startServer(options) {
381
382
  const count = relationships.getAll().length;
382
383
  console.log(pc.green(` Relationships loaded: ${count} tracked (${intelligenceMode})`));
383
384
  }
385
+ // Set up quota tracking if enabled
386
+ let quotaTracker;
387
+ if (config.monitoring?.quotaTracking) {
388
+ const quotaFile = config.monitoring.quotaStateFile
389
+ || path.join(config.stateDir, 'quota-state.json');
390
+ quotaTracker = new QuotaTracker({
391
+ quotaFile,
392
+ thresholds: config.scheduler?.quotaThresholds ?? { normal: 50, elevated: 60, critical: 80, shutdown: 95 },
393
+ });
394
+ console.log(pc.green(` Quota tracking enabled (${quotaFile})`));
395
+ }
384
396
  let scheduler;
385
397
  if (config.scheduler.enabled) {
386
398
  scheduler = new JobScheduler(config.scheduler, sessionManager, state, config.stateDir);
399
+ if (quotaTracker) {
400
+ scheduler.canRunJob = quotaTracker.canRunJob.bind(quotaTracker);
401
+ scheduler.setQuotaTracker(quotaTracker);
402
+ }
387
403
  scheduler.start();
388
404
  console.log(pc.green(' Scheduler started'));
389
405
  }
@@ -487,7 +503,7 @@ export async function startServer(options) {
487
503
  ...(config.evolution || {}),
488
504
  });
489
505
  console.log(pc.green(' Evolution system enabled'));
490
- const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker, publisher, viewer, tunnel, evolution });
506
+ const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker, quotaTracker, publisher, viewer, tunnel, evolution });
491
507
  await server.start();
492
508
  // Start tunnel AFTER server is listening
493
509
  if (tunnel) {
@@ -69,8 +69,23 @@ export async function runSetup(opts) {
69
69
  console.log(pc.dim(' Security is enforced through behavioral hooks, identity grounding, and'));
70
70
  console.log(pc.dim(' scoped access — not permission dialogs. See: README.md > Security Model'));
71
71
  console.log();
72
+ // Detect git context to pass to the conversational wizard
73
+ const projectDir = process.cwd();
74
+ let gitContext = '';
75
+ try {
76
+ const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
77
+ cwd: projectDir,
78
+ encoding: 'utf-8',
79
+ stdio: ['pipe', 'pipe', 'pipe'],
80
+ }).trim();
81
+ const repoName = path.basename(gitRoot);
82
+ gitContext = ` This directory is inside a git repository "${repoName}" at ${gitRoot}.`;
83
+ }
84
+ catch {
85
+ gitContext = ' This directory is NOT inside a git repository.';
86
+ }
72
87
  // Launch Claude Code from the instar package root (where .claude/skills/ lives)
73
- // and pass the target project directory in the prompt.
88
+ // and pass the target project directory + git context in the prompt.
74
89
  //
75
90
  // --dangerously-skip-permissions is required here because the setup wizard
76
91
  // runs in instar's OWN package directory (instarRoot), not the user's
@@ -78,10 +93,9 @@ export async function runSetup(opts) {
78
93
  // user's project directory, which breaks the interactive flow. The wizard
79
94
  // only writes to well-defined locations (.instar/, .claude/, CLAUDE.md).
80
95
  const instarRoot = findInstarRoot();
81
- const projectDir = process.cwd();
82
96
  const child = spawn(claudePath, [
83
97
  '--dangerously-skip-permissions',
84
- `/setup-wizard The project to set up is at: ${projectDir}`,
98
+ `/setup-wizard The project to set up is at: ${projectDir}.${gitContext}`,
85
99
  ], {
86
100
  cwd: instarRoot,
87
101
  stdio: 'inherit',
@@ -126,6 +140,22 @@ function findInstarRoot() {
126
140
  // Fallback: assume we're in dist/commands/ — go up to root
127
141
  return path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
128
142
  }
143
+ /**
144
+ * Detect whether the current directory is inside a git repository.
145
+ */
146
+ function detectGitRepo(dir) {
147
+ try {
148
+ const root = execFileSync('git', ['rev-parse', '--show-toplevel'], {
149
+ cwd: dir,
150
+ encoding: 'utf-8',
151
+ stdio: ['pipe', 'pipe', 'pipe'],
152
+ }).trim();
153
+ return { isRepo: true, repoRoot: root, repoName: path.basename(root) };
154
+ }
155
+ catch {
156
+ return { isRepo: false };
157
+ }
158
+ }
129
159
  /**
130
160
  * Classic inquirer-based setup wizard.
131
161
  * The original interactive setup experience.
@@ -133,7 +163,7 @@ function findInstarRoot() {
133
163
  async function runClassicSetup() {
134
164
  console.log();
135
165
  console.log(pc.bold(' Welcome to Instar'));
136
- console.log(pc.dim(' Persistent agent infrastructure for any Claude Code project'));
166
+ console.log(pc.dim(' Turn Claude Code into a persistent agent you talk to through Telegram.'));
137
167
  console.log();
138
168
  // ── Step 0: Check and install prerequisites ─────────────────────
139
169
  const prereqs = await ensurePrerequisites();
@@ -143,19 +173,59 @@ async function runClassicSetup() {
143
173
  const tmuxPath = prereqs.results.find(r => r.name === 'tmux').path;
144
174
  // Use a scoped name to avoid shadowing the outer runSetup's claudePath
145
175
  const claudePath = prereqs.results.find(r => r.name === 'Claude CLI').path;
146
- // ── Step 1: Project ──────────────────────────────────────────────
176
+ // ── Step 1: Detect context and determine mode ─────────────────
147
177
  const detectedDir = process.cwd();
148
- const detectedName = path.basename(detectedDir);
149
- const projectDir = detectedDir; // Always use cwd
150
- const projectName = await input({
151
- message: 'Project name',
152
- default: detectedName,
153
- });
178
+ const gitInfo = detectGitRepo(detectedDir);
179
+ let projectDir;
180
+ let projectName;
181
+ let isProjectAgent;
182
+ if (gitInfo.isRepo) {
183
+ // Inside a git repository — suggest project agent
184
+ console.log(` ${pc.green('✓')} Detected git repository: ${pc.cyan(gitInfo.repoName)}`);
185
+ console.log(pc.dim(` ${gitInfo.repoRoot}`));
186
+ console.log();
187
+ console.log(pc.dim(' Your agent will live alongside this project — monitoring, building,'));
188
+ console.log(pc.dim(' and maintaining it. You talk to it through Telegram.'));
189
+ console.log();
190
+ const useThisRepo = await confirm({
191
+ message: `Set up an agent for ${gitInfo.repoName}?`,
192
+ default: true,
193
+ });
194
+ if (useThisRepo) {
195
+ projectDir = gitInfo.repoRoot;
196
+ projectName = await input({
197
+ message: 'Agent name',
198
+ default: gitInfo.repoName,
199
+ });
200
+ isProjectAgent = true;
201
+ }
202
+ else {
203
+ // They want a general agent instead
204
+ projectName = await input({
205
+ message: 'What should your agent be called?',
206
+ default: 'my-agent',
207
+ });
208
+ projectDir = detectedDir;
209
+ isProjectAgent = false;
210
+ }
211
+ }
212
+ else {
213
+ // Not in a git repo — this is a general/personal agent
214
+ console.log(pc.dim(' No git repository detected — setting up a personal agent.'));
215
+ console.log(pc.dim(' A personal agent lives on your machine and you talk to it through Telegram.'));
216
+ console.log();
217
+ projectName = await input({
218
+ message: 'What should your agent be called?',
219
+ default: 'my-agent',
220
+ });
221
+ projectDir = detectedDir;
222
+ isProjectAgent = false;
223
+ }
154
224
  // Check if already initialized
155
225
  const stateDir = path.join(projectDir, '.instar');
156
226
  if (fs.existsSync(path.join(stateDir, 'config.json'))) {
157
227
  const overwrite = await confirm({
158
- message: 'Agent kit already initialized here. Reconfigure?',
228
+ message: 'Agent already initialized here. Reconfigure?',
159
229
  default: false,
160
230
  });
161
231
  if (!overwrite) {
@@ -163,7 +233,19 @@ async function runClassicSetup() {
163
233
  return;
164
234
  }
165
235
  }
166
- // ── Step 2: Server port + sessions ─────────────────────────────
236
+ // ── Step 2: Telegram the primary interface ───────────────────
237
+ console.log();
238
+ console.log(pc.bold(' Telegram — How You Talk to Your Agent'));
239
+ console.log();
240
+ console.log(pc.dim(' Once connected, you just talk — no commands, no terminal.'));
241
+ console.log(pc.dim(' Topic threads, message history, mobile access, proactive notifications.'));
242
+ if (!isProjectAgent) {
243
+ console.log();
244
+ console.log(pc.dim(' For a personal agent, Telegram IS the interface.'));
245
+ }
246
+ console.log();
247
+ const telegramConfig = await promptForTelegram();
248
+ // ── Step 3: Server config (sensible defaults) ──────────────────
167
249
  const port = await number({
168
250
  message: 'Server port',
169
251
  default: 4040,
@@ -182,14 +264,6 @@ async function runClassicSetup() {
182
264
  return true;
183
265
  },
184
266
  }) ?? 3;
185
- // ── Step 3: Telegram (BEFORE users, so we know context) ────────
186
- console.log();
187
- console.log(pc.bold(' Telegram — Where Your Agent Lives'));
188
- console.log(pc.dim(' Telegram is where the real experience begins.'));
189
- console.log(pc.dim(' Once connected, you just talk — no commands, no terminal.'));
190
- console.log(pc.dim(' Topic threads, message history, mobile access, proactive notifications.'));
191
- console.log();
192
- const telegramConfig = await promptForTelegram();
193
267
  // ── Step 4: User setup ─────────────────────────────────────────
194
268
  console.log();
195
269
  const addUser = await confirm({
@@ -197,8 +197,10 @@ export interface MessagingAdapter {
197
197
  resolveUser(channelIdentifier: string): Promise<string | null>;
198
198
  }
199
199
  export interface QuotaState {
200
- /** Current usage percentage (0-100) */
200
+ /** Current weekly usage percentage (0-100) */
201
201
  usagePercent: number;
202
+ /** 5-hour rolling rate limit utilization (0-100), if available */
203
+ fiveHourPercent?: number;
202
204
  /** When usage data was last updated */
203
205
  lastUpdated: string;
204
206
  /** Per-account breakdown if multi-account */
@@ -209,9 +211,18 @@ export interface QuotaState {
209
211
  export interface AccountQuota {
210
212
  email: string;
211
213
  usagePercent: number;
214
+ /** 5-hour rolling rate limit utilization for this account */
215
+ fiveHourPercent?: number;
212
216
  isActive: boolean;
213
217
  lastUpdated: string;
214
218
  }
219
+ /** Cause of a session's death, as classified by QuotaExhaustionDetector */
220
+ export type SessionDeathCause = 'quota_exhaustion' | 'context_exhausted' | 'crash' | 'timeout' | 'normal_exit' | 'unknown';
221
+ export interface SessionDeathClassification {
222
+ cause: SessionDeathCause;
223
+ confidence: 'high' | 'medium' | 'low';
224
+ detail: string;
225
+ }
215
226
  export interface HealthStatus {
216
227
  status: 'healthy' | 'degraded' | 'unhealthy';
217
228
  components: Record<string, ComponentHealth>;
package/dist/index.d.ts CHANGED
@@ -26,6 +26,8 @@ export type { RouteContext } from './server/routes.js';
26
26
  export { corsMiddleware, authMiddleware, rateLimiter, requestTimeout, errorHandler } from './server/middleware.js';
27
27
  export { HealthChecker } from './monitoring/HealthChecker.js';
28
28
  export { QuotaTracker } from './monitoring/QuotaTracker.js';
29
+ export type { RemoteQuotaResult } from './monitoring/QuotaTracker.js';
30
+ export { classifySessionDeath } from './monitoring/QuotaExhaustionDetector.js';
29
31
  export { SleepWakeDetector } from './core/SleepWakeDetector.js';
30
32
  export { TelegramAdapter } from './messaging/TelegramAdapter.js';
31
33
  export type { TelegramConfig } from './messaging/TelegramAdapter.js';
@@ -35,6 +37,6 @@ export { PrivateViewer } from './publishing/PrivateViewer.js';
35
37
  export type { PrivateView, PrivateViewerConfig } from './publishing/PrivateViewer.js';
36
38
  export { TunnelManager } from './tunnel/TunnelManager.js';
37
39
  export type { TunnelConfig, TunnelState } from './tunnel/TunnelManager.js';
38
- export type { Session, SessionStatus, SessionManagerConfig, ModelTier, JobDefinition, JobPriority, JobExecution, JobState, JobSchedulerConfig, UserProfile, UserChannel, UserPreferences, Message, OutgoingMessage, MessagingAdapter, MessagingAdapterConfig, QuotaState, AccountQuota, HealthStatus, ComponentHealth, ActivityEvent, InstarConfig, MonitoringConfig, RelationshipRecord, RelationshipManagerConfig, InteractionSummary, FeedbackItem, FeedbackConfig, UpdateInfo, UpdateResult, DispatchConfig, UpdateConfig, PublishingConfig, TunnelConfigType, SkipReason, SkipEvent, WorkloadSignal, AutoTuneState, IntelligenceProvider, IntelligenceOptions, EvolutionProposal, EvolutionType, EvolutionStatus, LearningEntry, LearningSource, CapabilityGap, GapCategory, ActionItem, EvolutionManagerConfig, } from './core/types.js';
40
+ export type { Session, SessionStatus, SessionManagerConfig, ModelTier, JobDefinition, JobPriority, JobExecution, JobState, JobSchedulerConfig, UserProfile, UserChannel, UserPreferences, Message, OutgoingMessage, MessagingAdapter, MessagingAdapterConfig, QuotaState, AccountQuota, SessionDeathCause, SessionDeathClassification, HealthStatus, ComponentHealth, ActivityEvent, InstarConfig, MonitoringConfig, RelationshipRecord, RelationshipManagerConfig, InteractionSummary, FeedbackItem, FeedbackConfig, UpdateInfo, UpdateResult, DispatchConfig, UpdateConfig, PublishingConfig, TunnelConfigType, SkipReason, SkipEvent, WorkloadSignal, AutoTuneState, IntelligenceProvider, IntelligenceOptions, EvolutionProposal, EvolutionType, EvolutionStatus, LearningEntry, LearningSource, CapabilityGap, GapCategory, ActionItem, EvolutionManagerConfig, } from './core/types.js';
39
41
  export type { Dispatch, DispatchCheckResult, DispatchEvaluation, EvaluationDecision, DispatchFeedback, DispatchStats } from './core/DispatchManager.js';
40
42
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ export { corsMiddleware, authMiddleware, rateLimiter, requestTimeout, errorHandl
28
28
  // Monitoring
29
29
  export { HealthChecker } from './monitoring/HealthChecker.js';
30
30
  export { QuotaTracker } from './monitoring/QuotaTracker.js';
31
+ export { classifySessionDeath } from './monitoring/QuotaExhaustionDetector.js';
31
32
  export { SleepWakeDetector } from './core/SleepWakeDetector.js';
32
33
  // Messaging
33
34
  export { TelegramAdapter } from './messaging/TelegramAdapter.js';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Quota Exhaustion Detector — classifies WHY a session died.
3
+ *
4
+ * Pattern-matches tmux output from a dead session against known
5
+ * failure signatures, cross-references with quota state to produce
6
+ * a confidence-weighted classification.
7
+ *
8
+ * Use cases:
9
+ * - Skip ledger: record quota_exhaustion vs crash vs normal_exit
10
+ * - Telegram alerts: "session died because quota exhausted"
11
+ * - Auto-recovery: trigger account switch on repeated quota deaths
12
+ *
13
+ * Ported from Dawn's dawn-server/src/quota/QuotaExhaustionDetector.ts,
14
+ * simplified for general Instar use (no Telegram dependency).
15
+ */
16
+ import type { QuotaState, SessionDeathClassification } from '../core/types.js';
17
+ /**
18
+ * Classify why a session died based on its terminal output and quota state.
19
+ */
20
+ export declare function classifySessionDeath(tmuxOutput: string, quotaState?: QuotaState | null): SessionDeathClassification;
21
+ //# sourceMappingURL=QuotaExhaustionDetector.d.ts.map
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Quota Exhaustion Detector — classifies WHY a session died.
3
+ *
4
+ * Pattern-matches tmux output from a dead session against known
5
+ * failure signatures, cross-references with quota state to produce
6
+ * a confidence-weighted classification.
7
+ *
8
+ * Use cases:
9
+ * - Skip ledger: record quota_exhaustion vs crash vs normal_exit
10
+ * - Telegram alerts: "session died because quota exhausted"
11
+ * - Auto-recovery: trigger account switch on repeated quota deaths
12
+ *
13
+ * Ported from Dawn's dawn-server/src/quota/QuotaExhaustionDetector.ts,
14
+ * simplified for general Instar use (no Telegram dependency).
15
+ */
16
+ // Pattern groups for classification
17
+ const QUOTA_PATTERNS = [
18
+ 'overloaded_error',
19
+ 'rate_limit',
20
+ '429',
21
+ 'quota.*exceeded',
22
+ 'usage.*limit',
23
+ 'too many requests',
24
+ 'resource_exhausted',
25
+ 'capacity',
26
+ 'throttl',
27
+ 'rate limit exceeded',
28
+ ];
29
+ const CONTEXT_PATTERNS = [
30
+ 'context.*exhaust',
31
+ 'context.*limit',
32
+ 'context.*full',
33
+ 'token.*limit.*reached',
34
+ 'maximum.*context',
35
+ 'conversation is too long',
36
+ ];
37
+ const CRASH_PATTERNS = [
38
+ 'SIGABRT',
39
+ 'SIGSEGV',
40
+ 'SIGKILL',
41
+ 'fatal error',
42
+ 'unhandled.*exception',
43
+ 'panic:',
44
+ 'stack overflow',
45
+ 'out of memory',
46
+ 'heap.*out',
47
+ ];
48
+ const NORMAL_EXIT_PATTERNS = [
49
+ 'Session ended',
50
+ 'Goodbye!',
51
+ 'has been completed',
52
+ 'Auto-exit',
53
+ '✓',
54
+ 'exited cleanly',
55
+ ];
56
+ /**
57
+ * Classify why a session died based on its terminal output and quota state.
58
+ */
59
+ export function classifySessionDeath(tmuxOutput, quotaState) {
60
+ if (!tmuxOutput || tmuxOutput.trim().length === 0) {
61
+ return { cause: 'unknown', confidence: 'low', detail: 'No output captured' };
62
+ }
63
+ const output = tmuxOutput.toLowerCase();
64
+ // Check each pattern group
65
+ const quotaMatch = matchPatterns(output, QUOTA_PATTERNS);
66
+ const contextMatch = matchPatterns(output, CONTEXT_PATTERNS);
67
+ const crashMatch = matchPatterns(output, CRASH_PATTERNS);
68
+ const normalMatch = matchPatterns(output, NORMAL_EXIT_PATTERNS);
69
+ // Normal exit takes precedence if present
70
+ if (normalMatch && !quotaMatch && !crashMatch) {
71
+ return {
72
+ cause: 'normal_exit',
73
+ confidence: 'high',
74
+ detail: `Matched normal exit pattern: "${normalMatch}"`,
75
+ };
76
+ }
77
+ // Quota exhaustion — cross-reference with state for confidence
78
+ if (quotaMatch) {
79
+ let confidence = 'medium';
80
+ // Cross-reference: if 5-hour is high, boost confidence
81
+ if (quotaState) {
82
+ const fiveHour = quotaState.fiveHourPercent;
83
+ const weekly = quotaState.usagePercent;
84
+ if (typeof fiveHour === 'number' && fiveHour > 90) {
85
+ confidence = 'high';
86
+ }
87
+ else if (weekly > 85) {
88
+ confidence = 'high';
89
+ }
90
+ }
91
+ return {
92
+ cause: 'quota_exhaustion',
93
+ confidence,
94
+ detail: `Matched quota pattern: "${quotaMatch}"` +
95
+ (quotaState ? ` (weekly: ${quotaState.usagePercent}%, 5h: ${quotaState.fiveHourPercent ?? 'n/a'}%)` : ''),
96
+ };
97
+ }
98
+ // Context exhaustion
99
+ if (contextMatch) {
100
+ return {
101
+ cause: 'context_exhausted',
102
+ confidence: 'medium',
103
+ detail: `Matched context pattern: "${contextMatch}"`,
104
+ };
105
+ }
106
+ // Crash
107
+ if (crashMatch) {
108
+ return {
109
+ cause: 'crash',
110
+ confidence: 'medium',
111
+ detail: `Matched crash pattern: "${crashMatch}"`,
112
+ };
113
+ }
114
+ return { cause: 'unknown', confidence: 'low', detail: 'No patterns matched' };
115
+ }
116
+ /**
117
+ * Check if output matches any pattern in a group.
118
+ * Returns the first matching pattern string, or null.
119
+ */
120
+ function matchPatterns(output, patterns) {
121
+ for (const pattern of patterns) {
122
+ try {
123
+ if (new RegExp(pattern, 'i').test(output)) {
124
+ return pattern;
125
+ }
126
+ }
127
+ catch {
128
+ // If regex is invalid, try simple string match
129
+ if (output.includes(pattern.toLowerCase())) {
130
+ return pattern;
131
+ }
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+ //# sourceMappingURL=QuotaExhaustionDetector.js.map
@@ -34,16 +34,28 @@ export declare class QuotaTracker {
34
34
  /**
35
35
  * Determine if a job at the given priority should run based on current quota.
36
36
  *
37
- * Threshold logic:
38
- * - Below normal (e.g. 50%): all jobs run
39
- * - Above normal but below elevated (e.g. 60%): high+ only
40
- * - Above elevated but below critical (e.g. 80%): critical only
41
- * - Above critical (e.g. 95%): no jobs
37
+ * Checks BOTH weekly usage AND 5-hour rate limit:
38
+ * - 5-hour >= 95%: block ALL spawns (sessions will immediately fail)
39
+ * - 5-hour >= 80%: only critical priority
40
+ * - Weekly >= shutdown (e.g. 95%): no jobs
41
+ * - Weekly >= critical (e.g. 80%): critical only
42
+ * - Weekly >= elevated (e.g. 60%): high+ only
43
+ * - Weekly >= normal (e.g. 50%): medium+ only
42
44
  *
43
45
  * If quota data is unavailable or stale, defaults to allowing all jobs
44
46
  * (fail-open — better to run than to silently stop).
45
47
  */
46
48
  canRunJob(priority: JobPriority): boolean;
49
+ /**
50
+ * Check if a session should be spawned at the given priority.
51
+ * Returns a structured result with reason — useful for logging and notifications.
52
+ *
53
+ * Checks both weekly AND 5-hour rate limits.
54
+ */
55
+ shouldSpawnSession(priority?: JobPriority): {
56
+ allowed: boolean;
57
+ reason: string;
58
+ };
47
59
  /**
48
60
  * Write a quota state to the file (for collector scripts or manual updates).
49
61
  */
@@ -52,5 +64,29 @@ export declare class QuotaTracker {
52
64
  * Get the recommendation string for display purposes.
53
65
  */
54
66
  getRecommendation(): QuotaState['recommendation'];
67
+ /**
68
+ * Fetch quota status from a remote API (e.g., Dawn's /api/instar/quota).
69
+ * If the remote says canProceed=false, updates local state accordingly.
70
+ *
71
+ * This allows Instar agents to check a central quota authority before
72
+ * spawning sessions, preventing wasted attempts on exhausted machines.
73
+ *
74
+ * @param url - Full URL to the quota API (e.g., "https://dawn.bot-me.ai/api/instar/quota")
75
+ * @param apiKey - Authorization token (sent as Bearer header)
76
+ * @param timeoutMs - Request timeout (default 5000ms)
77
+ * @returns Remote quota status, or null on failure (fail-open)
78
+ */
79
+ fetchRemoteQuota(url: string, apiKey: string, timeoutMs?: number): Promise<RemoteQuotaResult | null>;
80
+ }
81
+ /** Result from a remote quota API (e.g., /api/instar/quota) */
82
+ export interface RemoteQuotaResult {
83
+ canProceed: boolean;
84
+ blockReason?: string | null;
85
+ activeAccount?: string | null;
86
+ weeklyPercent: number;
87
+ fiveHourPercent?: number | null;
88
+ canRunPriority?: string;
89
+ recommendation?: string | null;
90
+ stale?: boolean;
55
91
  }
56
92
  //# sourceMappingURL=QuotaTracker.d.ts.map
@@ -60,36 +60,70 @@ export class QuotaTracker {
60
60
  /**
61
61
  * Determine if a job at the given priority should run based on current quota.
62
62
  *
63
- * Threshold logic:
64
- * - Below normal (e.g. 50%): all jobs run
65
- * - Above normal but below elevated (e.g. 60%): high+ only
66
- * - Above elevated but below critical (e.g. 80%): critical only
67
- * - Above critical (e.g. 95%): no jobs
63
+ * Checks BOTH weekly usage AND 5-hour rate limit:
64
+ * - 5-hour >= 95%: block ALL spawns (sessions will immediately fail)
65
+ * - 5-hour >= 80%: only critical priority
66
+ * - Weekly >= shutdown (e.g. 95%): no jobs
67
+ * - Weekly >= critical (e.g. 80%): critical only
68
+ * - Weekly >= elevated (e.g. 60%): high+ only
69
+ * - Weekly >= normal (e.g. 50%): medium+ only
68
70
  *
69
71
  * If quota data is unavailable or stale, defaults to allowing all jobs
70
72
  * (fail-open — better to run than to silently stop).
71
73
  */
72
74
  canRunJob(priority) {
75
+ const result = this.shouldSpawnSession(priority);
76
+ return result.allowed;
77
+ }
78
+ /**
79
+ * Check if a session should be spawned at the given priority.
80
+ * Returns a structured result with reason — useful for logging and notifications.
81
+ *
82
+ * Checks both weekly AND 5-hour rate limits.
83
+ */
84
+ shouldSpawnSession(priority) {
73
85
  const state = this.getState();
74
86
  if (!state)
75
- return true; // No data fail open
87
+ return { allowed: true, reason: 'No quota data fail open' };
88
+ // Check 5-hour rate limit first — these cause immediate session failures
89
+ const fiveHour = state.fiveHourPercent;
90
+ if (typeof fiveHour === 'number' && isFinite(fiveHour)) {
91
+ if (fiveHour >= 95) {
92
+ return { allowed: false, reason: `5-hour rate limit at ${fiveHour}% — sessions will fail immediately` };
93
+ }
94
+ if (fiveHour >= 80 && priority && priority !== 'critical') {
95
+ return { allowed: false, reason: `5-hour rate limit at ${fiveHour}% — only critical priority allowed` };
96
+ }
97
+ }
98
+ // Check weekly usage
76
99
  const rawUsage = state.usagePercent;
77
- if (typeof rawUsage !== 'number' || !isFinite(rawUsage))
78
- return true; // Bad data fail open
100
+ if (typeof rawUsage !== 'number' || !isFinite(rawUsage)) {
101
+ return { allowed: true, reason: 'Invalid weekly data fail open' };
102
+ }
79
103
  const usage = Math.max(0, Math.min(100, rawUsage));
80
104
  const { normal, elevated, critical, shutdown } = this.config.thresholds;
81
- if (usage >= shutdown)
82
- return false; // Nothing runs
105
+ if (usage >= shutdown) {
106
+ return { allowed: false, reason: `Weekly quota at ${usage}% — all jobs stopped` };
107
+ }
83
108
  if (usage >= critical) {
84
- return priority === 'critical';
109
+ const ok = !priority || priority === 'critical';
110
+ return ok
111
+ ? { allowed: true, reason: `Weekly at ${usage}% — critical only` }
112
+ : { allowed: false, reason: `Weekly quota at ${usage}% — only critical priority runs` };
85
113
  }
86
114
  if (usage >= elevated) {
87
- return priority === 'critical' || priority === 'high';
115
+ const ok = !priority || priority === 'critical' || priority === 'high';
116
+ return ok
117
+ ? { allowed: true, reason: `Weekly at ${usage}% — high+ only` }
118
+ : { allowed: false, reason: `Weekly quota at ${usage}% — only high+ priority runs` };
88
119
  }
89
120
  if (usage >= normal) {
90
- return priority !== 'low';
121
+ const ok = !priority || priority !== 'low';
122
+ return ok
123
+ ? { allowed: true, reason: `Weekly at ${usage}% — medium+ only` }
124
+ : { allowed: false, reason: `Weekly quota at ${usage}% — low priority paused` };
91
125
  }
92
- return true; // Below normal — everything runs
126
+ return { allowed: true, reason: 'Quota normal' };
93
127
  }
94
128
  /**
95
129
  * Write a quota state to the file (for collector scripts or manual updates).
@@ -126,6 +160,11 @@ export class QuotaTracker {
126
160
  const state = this.getState();
127
161
  if (!state)
128
162
  return 'normal';
163
+ // 5-hour at 95%+ is always 'stop' regardless of weekly
164
+ if (typeof state.fiveHourPercent === 'number' && state.fiveHourPercent >= 95)
165
+ return 'stop';
166
+ if (typeof state.fiveHourPercent === 'number' && state.fiveHourPercent >= 80)
167
+ return 'critical';
129
168
  const usage = state.usagePercent;
130
169
  const { normal, elevated, critical, shutdown } = this.config.thresholds;
131
170
  if (usage >= shutdown)
@@ -138,5 +177,55 @@ export class QuotaTracker {
138
177
  return 'reduce';
139
178
  return 'normal';
140
179
  }
180
+ /**
181
+ * Fetch quota status from a remote API (e.g., Dawn's /api/instar/quota).
182
+ * If the remote says canProceed=false, updates local state accordingly.
183
+ *
184
+ * This allows Instar agents to check a central quota authority before
185
+ * spawning sessions, preventing wasted attempts on exhausted machines.
186
+ *
187
+ * @param url - Full URL to the quota API (e.g., "https://dawn.bot-me.ai/api/instar/quota")
188
+ * @param apiKey - Authorization token (sent as Bearer header)
189
+ * @param timeoutMs - Request timeout (default 5000ms)
190
+ * @returns Remote quota status, or null on failure (fail-open)
191
+ */
192
+ async fetchRemoteQuota(url, apiKey, timeoutMs = 5000) {
193
+ try {
194
+ const controller = new AbortController();
195
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
196
+ const response = await fetch(url, {
197
+ headers: {
198
+ 'Authorization': `Bearer ${apiKey}`,
199
+ 'Accept': 'application/json',
200
+ },
201
+ signal: controller.signal,
202
+ });
203
+ clearTimeout(timer);
204
+ if (!response.ok) {
205
+ console.warn(`[quota] Remote quota API returned ${response.status}`);
206
+ return null;
207
+ }
208
+ const data = await response.json();
209
+ // If remote says blocked, update local state to reflect it
210
+ if (!data.canProceed && typeof data.weeklyPercent === 'number') {
211
+ const state = this.getState() ?? {
212
+ usagePercent: 0,
213
+ lastUpdated: new Date().toISOString(),
214
+ };
215
+ state.usagePercent = data.weeklyPercent;
216
+ if (typeof data.fiveHourPercent === 'number') {
217
+ state.fiveHourPercent = data.fiveHourPercent;
218
+ }
219
+ state.lastUpdated = new Date().toISOString();
220
+ this.cachedState = state;
221
+ }
222
+ return data;
223
+ }
224
+ catch (err) {
225
+ // Network failure, timeout, etc. — fail open
226
+ console.warn(`[quota] Remote quota check failed: ${err instanceof Error ? err.message : err}`);
227
+ return null;
228
+ }
229
+ }
141
230
  }
142
231
  //# sourceMappingURL=QuotaTracker.js.map
@@ -10,6 +10,7 @@
10
10
  import { SkipLedger } from './SkipLedger.js';
11
11
  import type { SessionManager } from '../core/SessionManager.js';
12
12
  import type { StateManager } from '../core/StateManager.js';
13
+ import type { QuotaTracker } from '../monitoring/QuotaTracker.js';
13
14
  import type { MessagingAdapter } from '../core/types.js';
14
15
  import type { JobDefinition, JobSchedulerConfig, JobPriority } from '../core/types.js';
15
16
  import type { TelegramAdapter } from '../messaging/TelegramAdapter.js';
@@ -42,6 +43,8 @@ export declare class JobScheduler {
42
43
  private messenger;
43
44
  /** Optional Telegram adapter for job-topic coupling */
44
45
  private telegram;
46
+ /** Optional quota tracker for death classification cross-reference */
47
+ private quotaTracker;
45
48
  constructor(config: JobSchedulerConfig, sessionManager: SessionManager, state: StateManager, stateDir: string);
46
49
  /**
47
50
  * Set a messaging adapter for job completion notifications.
@@ -52,6 +55,12 @@ export declare class JobScheduler {
52
55
  * Every job gets its own topic — the user's window into the job.
53
56
  */
54
57
  setTelegram(adapter: TelegramAdapter): void;
58
+ /**
59
+ * Set the quota tracker for session death classification.
60
+ * When set, session deaths are cross-referenced with quota state
61
+ * to determine if they died from quota exhaustion.
62
+ */
63
+ setQuotaTracker(tracker: QuotaTracker): void;
55
64
  /**
56
65
  * Start the scheduler — load jobs, set up cron tasks, check for missed jobs.
57
66
  */
@@ -11,6 +11,7 @@ import { Cron } from 'croner';
11
11
  import { execFileSync } from 'node:child_process';
12
12
  import { loadJobs } from './JobLoader.js';
13
13
  import { SkipLedger } from './SkipLedger.js';
14
+ import { classifySessionDeath } from '../monitoring/QuotaExhaustionDetector.js';
14
15
  const PRIORITY_ORDER = {
15
16
  critical: 0,
16
17
  high: 1,
@@ -33,6 +34,8 @@ export class JobScheduler {
33
34
  messenger = null;
34
35
  /** Optional Telegram adapter for job-topic coupling */
35
36
  telegram = null;
37
+ /** Optional quota tracker for death classification cross-reference */
38
+ quotaTracker = null;
36
39
  constructor(config, sessionManager, state, stateDir) {
37
40
  this.config = config;
38
41
  this.sessionManager = sessionManager;
@@ -52,6 +55,14 @@ export class JobScheduler {
52
55
  setTelegram(adapter) {
53
56
  this.telegram = adapter;
54
57
  }
58
+ /**
59
+ * Set the quota tracker for session death classification.
60
+ * When set, session deaths are cross-referenced with quota state
61
+ * to determine if they died from quota exhaustion.
62
+ */
63
+ setQuotaTracker(tracker) {
64
+ this.quotaTracker = tracker;
65
+ }
55
66
  /**
56
67
  * Start the scheduler — load jobs, set up cron tasks, check for missed jobs.
57
68
  */
@@ -353,6 +364,24 @@ export class JobScheduler {
353
364
  catch {
354
365
  // Session may already be dead — that's fine
355
366
  }
367
+ // Classify death cause if session failed/was killed
368
+ let deathCause;
369
+ if (failed && output) {
370
+ const quotaState = this.quotaTracker?.getState() ?? null;
371
+ const classification = classifySessionDeath(output, quotaState);
372
+ deathCause = classification.cause;
373
+ this.state.appendEvent({
374
+ type: 'session_death_classified',
375
+ summary: `Session for "${job.slug}" classified as ${classification.cause} (${classification.confidence}): ${classification.detail}`,
376
+ timestamp: new Date().toISOString(),
377
+ metadata: {
378
+ slug: job.slug,
379
+ cause: classification.cause,
380
+ confidence: classification.confidence,
381
+ detail: classification.detail,
382
+ },
383
+ });
384
+ }
356
385
  // Build a summary message
357
386
  const duration = session.startedAt
358
387
  ? Math.round((Date.now() - new Date(session.startedAt).getTime()) / 1000)
@@ -361,7 +390,10 @@ export class JobScheduler {
361
390
  ? `${Math.floor(duration / 60)}m ${duration % 60}s`
362
391
  : `${duration}s`;
363
392
  let summary = `*Job Complete: ${job.name}*\n`;
364
- summary += `Status: ${failed ? 'Failed' : 'Done'}\n`;
393
+ summary += `Status: ${failed ? 'Failed' : 'Done'}`;
394
+ if (deathCause && deathCause !== 'unknown')
395
+ summary += ` (${deathCause})`;
396
+ summary += '\n';
365
397
  if (duration > 0)
366
398
  summary += `Duration: ${durationStr}\n`;
367
399
  if (output) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.6.15",
3
+ "version": "0.7.1",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,11 +0,0 @@
1
- > Why do I have a folder named ".vercel" in my project?
2
- The ".vercel" folder is created when you link a directory to a Vercel project.
3
-
4
- > What does the "project.json" file contain?
5
- The "project.json" file contains:
6
- - The ID of the Vercel project that you linked ("projectId")
7
- - The ID of the user or team your Vercel project is owned by ("orgId")
8
-
9
- > Should I commit the ".vercel" folder?
10
- No, you should not share the ".vercel" folder with anyone.
11
- Upon creation, it will be automatically added to your ".gitignore" file.
@@ -1 +0,0 @@
1
- {"projectId":"prj_evM5LcItYL3IAmw8zNvEPGrHeaya","orgId":"team_dHctwIDcV3X9ydapQlCPHFGI","projectName":"claude-agent-kit"}