instar 0.7.22 → 0.7.24

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.
@@ -594,9 +594,9 @@ Instar has a built-in feedback loop. When something isn't working, your user can
594
594
  - User complains to you naturally about an issue or missing capability
595
595
  - You route the feedback to the Instar maintainers with context
596
596
  - A fix or improvement is published as a new npm version
597
- - The \`update-check\` job detects the new version and notifies your user
597
+ - The built-in auto-updater detects the new version, applies it, notifies your user, and restarts the server
598
598
 
599
- **A rising tide lifts all ships** — every user's feedback improves the platform for everyone. Report issues when you encounter them. Check for updates regularly.
599
+ **A rising tide lifts all ships** — every user's feedback improves the platform for everyone. Updates are applied automatically.
600
600
 
601
601
  - Report: \`curl -X POST http://localhost:${port}/feedback -d '{"issue":"description","context":"relevant logs"}'\`
602
602
  - Check updates: \`npm outdated -g instar\`
@@ -930,7 +930,7 @@ curl -s -X POST http://localhost:${port}/feedback/retry
930
930
 
931
931
  ## How It Works
932
932
 
933
- Your feedback is stored locally AND forwarded to the instar maintainers. When they fix the issue and publish an update, your update-check job detects it automatically. One agent's bug report lifts all ships.
933
+ Your feedback is stored locally AND forwarded to the instar maintainers. When they fix the issue and publish an update, the built-in auto-updater detects it, applies it, and restarts the server — no manual intervention needed. One agent's bug report lifts all ships.
934
934
 
935
935
  **User feedback matters too.** When your user says "this isn't working" or "I wish I could..." — capture it with their original words. User language carries context that technical rephrasing loses.
936
936
  `,
@@ -995,16 +995,16 @@ function getDefaultJobs(port) {
995
995
  {
996
996
  slug: 'update-check',
997
997
  name: 'Update Check',
998
- description: 'Check if a newer version of instar is available. Understand what changed, notify the user, and apply the update. Runs frequently during early adoption.',
998
+ description: 'Legacy update check job disabled because the built-in AutoUpdater handles updates automatically (check, apply, notify, restart). See GET /updates/auto for status.',
999
999
  schedule: '*/30 * * * *',
1000
1000
  priority: 'medium',
1001
1001
  expectedDurationMinutes: 2,
1002
1002
  model: 'haiku',
1003
- enabled: true,
1003
+ enabled: false,
1004
1004
  gate: `curl -sf http://localhost:${port}/updates 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('updateAvailable') else 1)"`,
1005
1005
  execute: {
1006
- type: 'prompt',
1007
- value: `Check for instar updates: curl -s http://localhost:${port}/updates. If updateAvailable is false, exit silently — do NOT notify the user or produce any output. If updateAvailable is true: 1) Read the changeSummary to understand what changed. 2) Apply the update immediately: curl -s -X POST http://localhost:${port}/updates/apply. 3) After successful apply, notify the user via Telegram (if configured) with a brief, conversational message: what version was installed, what's new (plain language, not jargon), and that a server restart is needed if restartNeeded is true. 4) If the update fails, notify the user with the error. Rollback is available: curl -s -X POST http://localhost:${port}/updates/rollback. Keep this lightweight — no output when there's nothing to report.`,
1006
+ type: 'script',
1007
+ value: `curl -s http://localhost:${port}/updates/auto`,
1008
1008
  },
1009
1009
  tags: ['coherence', 'default'],
1010
1010
  },
@@ -23,6 +23,7 @@ import { AnthropicIntelligenceProvider } from '../core/AnthropicIntelligenceProv
23
23
  import { FeedbackManager } from '../core/FeedbackManager.js';
24
24
  import { DispatchManager } from '../core/DispatchManager.js';
25
25
  import { UpdateChecker } from '../core/UpdateChecker.js';
26
+ import { AutoUpdater } from '../core/AutoUpdater.js';
26
27
  import { registerPort, unregisterPort, startHeartbeat } from '../core/PortRegistry.js';
27
28
  import { TelegraphService } from '../publishing/TelegraphService.js';
28
29
  import { PrivateViewer } from '../publishing/PrivateViewer.js';
@@ -459,16 +460,22 @@ export async function startServer(options) {
459
460
  hasTelegram: config.messaging.some(m => m.type === 'telegram' && m.enabled),
460
461
  projectName: config.projectName,
461
462
  });
462
- // Check for updates on startup
463
+ // Check for updates on startup (non-blocking)
463
464
  updateChecker.check().then(info => {
464
465
  if (info.updateAvailable) {
465
466
  console.log(pc.yellow(` Update available: ${info.currentVersion} → ${info.latestVersion}`));
466
- console.log(pc.yellow(` Run: npm update -g instar`));
467
467
  }
468
468
  else {
469
469
  console.log(pc.green(` Instar ${info.currentVersion} is up to date`));
470
470
  }
471
471
  }).catch(() => { });
472
+ // Start auto-updater — periodic check + auto-apply + notify + restart
473
+ const autoUpdater = new AutoUpdater(updateChecker, state, config.stateDir, {
474
+ checkIntervalMinutes: 30,
475
+ autoApply: config.updates?.autoApply ?? true,
476
+ autoRestart: true,
477
+ }, telegram);
478
+ autoUpdater.start();
472
479
  // Set up Telegraph publishing (auto-enabled when config exists or Telegram is configured)
473
480
  let publisher;
474
481
  const pubConfig = config.publishing;
@@ -503,7 +510,7 @@ export async function startServer(options) {
503
510
  ...(config.evolution || {}),
504
511
  });
505
512
  console.log(pc.green(' Evolution system enabled'));
506
- const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker, quotaTracker, publisher, viewer, tunnel, evolution });
513
+ const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker, autoUpdater, quotaTracker, publisher, viewer, tunnel, evolution });
507
514
  await server.start();
508
515
  // Start tunnel AFTER server is listening
509
516
  if (tunnel) {
@@ -519,6 +526,7 @@ export async function startServer(options) {
519
526
  // Graceful shutdown
520
527
  const shutdown = async () => {
521
528
  console.log('\nShutting down...');
529
+ autoUpdater.stop();
522
530
  if (tunnel)
523
531
  await tunnel.stop();
524
532
  stopHeartbeat();
@@ -1300,9 +1300,9 @@ Instar has a built-in feedback loop — a rising tide that lifts all ships. When
1300
1300
  - User complains naturally about an issue or missing capability
1301
1301
  - Agent packages the issue with context and routes it upstream
1302
1302
  - A fix is published as a new npm version
1303
- - The \`update-check\` job detects the new version and notifies the user
1303
+ - The built-in auto-updater detects the new version, applies it, notifies the user, and restarts the server
1304
1304
 
1305
- Every user's feedback makes the platform better for everyone. Report issues when you encounter them. Check for updates regularly with \`npm outdated -g instar\`.
1305
+ Every user's feedback makes the platform better for everyone. Report issues when you encounter them. Updates are applied automatically — check status with \`curl http://localhost:PORT/updates/auto\`.
1306
1306
 
1307
1307
  ### Self-Evolution
1308
1308
 
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Auto Updater — built-in periodic update mechanism.
3
+ *
4
+ * Runs inside the server process (no Claude session needed).
5
+ * Periodically checks for updates, auto-applies when available,
6
+ * notifies via Telegram, and handles server restart.
7
+ *
8
+ * This replaces the heavyweight prompt-based update-check job.
9
+ * Updates should never depend on the job scheduler — they're
10
+ * core infrastructure that must run independently.
11
+ *
12
+ * Flow:
13
+ * check → apply → migrate → notify → restart
14
+ *
15
+ * Restart strategy:
16
+ * After npm update replaces the CLI on disk, spawn a replacement
17
+ * server process and exit. The new process binds to the port after
18
+ * the old one releases it during shutdown.
19
+ */
20
+ import type { UpdateChecker } from './UpdateChecker.js';
21
+ import type { TelegramAdapter } from '../messaging/TelegramAdapter.js';
22
+ import type { StateManager } from './StateManager.js';
23
+ export interface AutoUpdaterConfig {
24
+ /** How often to check for updates, in minutes. Default: 30 */
25
+ checkIntervalMinutes?: number;
26
+ /** Whether to auto-apply updates. Default: true */
27
+ autoApply?: boolean;
28
+ /** Telegram topic ID for update notifications (uses Agent Attention if not set) */
29
+ notificationTopicId?: number;
30
+ /** Whether to auto-restart after applying an update. Default: true */
31
+ autoRestart?: boolean;
32
+ }
33
+ export interface AutoUpdaterStatus {
34
+ /** Whether the auto-updater is running */
35
+ running: boolean;
36
+ /** Last time we checked for updates */
37
+ lastCheck: string | null;
38
+ /** Last time we applied an update */
39
+ lastApply: string | null;
40
+ /** Current configuration */
41
+ config: Required<AutoUpdaterConfig>;
42
+ /** Any pending update that hasn't been applied yet */
43
+ pendingUpdate: string | null;
44
+ /** Last error if any */
45
+ lastError: string | null;
46
+ }
47
+ export declare class AutoUpdater {
48
+ private updateChecker;
49
+ private telegram;
50
+ private state;
51
+ private config;
52
+ private interval;
53
+ private lastCheck;
54
+ private lastApply;
55
+ private lastError;
56
+ private pendingUpdate;
57
+ private isApplying;
58
+ private stateFile;
59
+ constructor(updateChecker: UpdateChecker, state: StateManager, stateDir: string, config?: AutoUpdaterConfig, telegram?: TelegramAdapter | null);
60
+ /**
61
+ * Start the periodic update checker.
62
+ * Idempotent — calling start() when already running is a no-op.
63
+ */
64
+ start(): void;
65
+ /**
66
+ * Stop the periodic checker.
67
+ */
68
+ stop(): void;
69
+ /**
70
+ * Get current auto-updater status.
71
+ */
72
+ getStatus(): AutoUpdaterStatus;
73
+ /**
74
+ * Set the Telegram adapter (may be wired after construction).
75
+ */
76
+ setTelegram(telegram: TelegramAdapter): void;
77
+ /**
78
+ * One tick of the update loop.
79
+ * Check → optionally apply → notify → optionally restart.
80
+ */
81
+ private tick;
82
+ /**
83
+ * Self-restart the server after an update.
84
+ *
85
+ * Strategy:
86
+ * 1. Spawn a shell that waits 2 seconds (for port release), then
87
+ * starts the new server version using the same CLI arguments.
88
+ * 2. Send SIGTERM to ourselves to trigger graceful shutdown.
89
+ *
90
+ * The 2-second delay ensures the old process has time to release
91
+ * the port before the new one tries to bind.
92
+ *
93
+ * If running in tmux, the replacement process inherits the PTY.
94
+ * If running under a process manager (launchd, systemd), the
95
+ * manager handles restart automatically after we exit.
96
+ */
97
+ private selfRestart;
98
+ /**
99
+ * Send a notification via Telegram (if configured).
100
+ * Falls back to console logging if Telegram is not available.
101
+ */
102
+ private notify;
103
+ /**
104
+ * Get the Agent Attention topic ID from state (where infrastructure notifications go).
105
+ */
106
+ private getAttentionTopicId;
107
+ /**
108
+ * Get the server port from the update checker config (for notification messages).
109
+ */
110
+ private getPort;
111
+ private loadState;
112
+ private saveState;
113
+ }
114
+ //# sourceMappingURL=AutoUpdater.d.ts.map
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Auto Updater — built-in periodic update mechanism.
3
+ *
4
+ * Runs inside the server process (no Claude session needed).
5
+ * Periodically checks for updates, auto-applies when available,
6
+ * notifies via Telegram, and handles server restart.
7
+ *
8
+ * This replaces the heavyweight prompt-based update-check job.
9
+ * Updates should never depend on the job scheduler — they're
10
+ * core infrastructure that must run independently.
11
+ *
12
+ * Flow:
13
+ * check → apply → migrate → notify → restart
14
+ *
15
+ * Restart strategy:
16
+ * After npm update replaces the CLI on disk, spawn a replacement
17
+ * server process and exit. The new process binds to the port after
18
+ * the old one releases it during shutdown.
19
+ */
20
+ import { spawn } from 'node:child_process';
21
+ import fs from 'node:fs';
22
+ import path from 'node:path';
23
+ export class AutoUpdater {
24
+ updateChecker;
25
+ telegram;
26
+ state;
27
+ config;
28
+ interval = null;
29
+ lastCheck = null;
30
+ lastApply = null;
31
+ lastError = null;
32
+ pendingUpdate = null;
33
+ isApplying = false;
34
+ stateFile;
35
+ constructor(updateChecker, state, stateDir, config, telegram) {
36
+ this.updateChecker = updateChecker;
37
+ this.state = state;
38
+ this.telegram = telegram ?? null;
39
+ this.stateFile = path.join(stateDir, 'state', 'auto-updater.json');
40
+ this.config = {
41
+ checkIntervalMinutes: config?.checkIntervalMinutes ?? 30,
42
+ autoApply: config?.autoApply ?? true,
43
+ autoRestart: config?.autoRestart ?? true,
44
+ notificationTopicId: config?.notificationTopicId ?? 0,
45
+ };
46
+ // Load persisted state (survives restarts)
47
+ this.loadState();
48
+ }
49
+ /**
50
+ * Start the periodic update checker.
51
+ * Idempotent — calling start() when already running is a no-op.
52
+ */
53
+ start() {
54
+ if (this.interval)
55
+ return;
56
+ const intervalMs = this.config.checkIntervalMinutes * 60 * 1000;
57
+ console.log(`[AutoUpdater] Started (every ${this.config.checkIntervalMinutes}m, ` +
58
+ `autoApply: ${this.config.autoApply}, autoRestart: ${this.config.autoRestart})`);
59
+ // Run first check after a short delay (don't block startup)
60
+ setTimeout(() => this.tick(), 10_000);
61
+ // Then run periodically
62
+ this.interval = setInterval(() => this.tick(), intervalMs);
63
+ this.interval.unref(); // Don't prevent process exit
64
+ }
65
+ /**
66
+ * Stop the periodic checker.
67
+ */
68
+ stop() {
69
+ if (this.interval) {
70
+ clearInterval(this.interval);
71
+ this.interval = null;
72
+ }
73
+ }
74
+ /**
75
+ * Get current auto-updater status.
76
+ */
77
+ getStatus() {
78
+ return {
79
+ running: this.interval !== null,
80
+ lastCheck: this.lastCheck,
81
+ lastApply: this.lastApply,
82
+ config: { ...this.config },
83
+ pendingUpdate: this.pendingUpdate,
84
+ lastError: this.lastError,
85
+ };
86
+ }
87
+ /**
88
+ * Set the Telegram adapter (may be wired after construction).
89
+ */
90
+ setTelegram(telegram) {
91
+ this.telegram = telegram;
92
+ }
93
+ /**
94
+ * One tick of the update loop.
95
+ * Check → optionally apply → notify → optionally restart.
96
+ */
97
+ async tick() {
98
+ if (this.isApplying) {
99
+ console.log('[AutoUpdater] Skipping tick — update already in progress');
100
+ return;
101
+ }
102
+ try {
103
+ // Step 1: Check for updates
104
+ const info = await this.updateChecker.check();
105
+ this.lastCheck = new Date().toISOString();
106
+ this.lastError = null;
107
+ if (!info.updateAvailable) {
108
+ this.pendingUpdate = null;
109
+ this.saveState();
110
+ return;
111
+ }
112
+ console.log(`[AutoUpdater] Update available: ${info.currentVersion} → ${info.latestVersion}`);
113
+ this.pendingUpdate = info.latestVersion;
114
+ this.saveState();
115
+ // Step 2: Auto-apply if configured
116
+ if (!this.config.autoApply) {
117
+ // Just notify — don't apply
118
+ await this.notify(`Update available: v${info.currentVersion} → v${info.latestVersion}\n\n` +
119
+ (info.changeSummary ? `Changes: ${info.changeSummary}\n\n` : '') +
120
+ `Auto-apply is disabled. Apply manually:\n` +
121
+ `curl -X POST http://localhost:${this.getPort()}/updates/apply`);
122
+ return;
123
+ }
124
+ // Step 3: Apply the update
125
+ this.isApplying = true;
126
+ console.log(`[AutoUpdater] Applying update to v${info.latestVersion}...`);
127
+ const result = await this.updateChecker.applyUpdate();
128
+ this.isApplying = false;
129
+ if (!result.success) {
130
+ this.lastError = result.message;
131
+ this.saveState();
132
+ console.error(`[AutoUpdater] Update failed: ${result.message}`);
133
+ await this.notify(`Update to v${info.latestVersion} failed: ${result.message}\n\n` +
134
+ `The current version (v${result.previousVersion}) is still running.`);
135
+ return;
136
+ }
137
+ // Step 4: Update succeeded
138
+ this.lastApply = new Date().toISOString();
139
+ this.pendingUpdate = null;
140
+ this.saveState();
141
+ console.log(`[AutoUpdater] Updated: v${result.previousVersion} → v${result.newVersion}`);
142
+ // Step 5: Notify via Telegram
143
+ const restartNote = result.restartNeeded && this.config.autoRestart
144
+ ? 'Server is restarting now...'
145
+ : result.restartNeeded
146
+ ? 'A server restart is needed to use the new version.'
147
+ : '';
148
+ await this.notify(`Updated: v${result.previousVersion} → v${result.newVersion}\n\n` +
149
+ (info.changeSummary ? `What changed:\n${info.changeSummary}\n\n` : '') +
150
+ restartNote);
151
+ // Step 6: Self-restart if needed and configured
152
+ if (result.restartNeeded && this.config.autoRestart) {
153
+ // Brief delay to let the Telegram notification send
154
+ await new Promise(r => setTimeout(r, 2000));
155
+ this.selfRestart();
156
+ }
157
+ }
158
+ catch (err) {
159
+ this.isApplying = false;
160
+ this.lastError = err instanceof Error ? err.message : String(err);
161
+ this.saveState();
162
+ console.error(`[AutoUpdater] Tick error: ${this.lastError}`);
163
+ }
164
+ }
165
+ /**
166
+ * Self-restart the server after an update.
167
+ *
168
+ * Strategy:
169
+ * 1. Spawn a shell that waits 2 seconds (for port release), then
170
+ * starts the new server version using the same CLI arguments.
171
+ * 2. Send SIGTERM to ourselves to trigger graceful shutdown.
172
+ *
173
+ * The 2-second delay ensures the old process has time to release
174
+ * the port before the new one tries to bind.
175
+ *
176
+ * If running in tmux, the replacement process inherits the PTY.
177
+ * If running under a process manager (launchd, systemd), the
178
+ * manager handles restart automatically after we exit.
179
+ */
180
+ selfRestart() {
181
+ console.log('[AutoUpdater] Initiating self-restart...');
182
+ // Build the command to restart
183
+ // process.argv[0] = node binary
184
+ // process.argv[1..] = CLI args (e.g., /path/to/cli.js server start --foreground)
185
+ const args = process.argv.slice(1)
186
+ .map(a => `'${a.replace(/'/g, "'\\''")}'`)
187
+ .join(' ');
188
+ const cmd = `sleep 2 && exec ${process.execPath} ${args}`;
189
+ try {
190
+ const child = spawn('sh', ['-c', cmd], {
191
+ detached: true,
192
+ stdio: 'inherit',
193
+ cwd: process.cwd(),
194
+ env: process.env,
195
+ });
196
+ child.unref();
197
+ console.log('[AutoUpdater] Replacement process spawned. Shutting down...');
198
+ // Trigger graceful shutdown (the SIGTERM handler in server.ts will clean up)
199
+ process.kill(process.pid, 'SIGTERM');
200
+ }
201
+ catch (err) {
202
+ console.error(`[AutoUpdater] Self-restart failed: ${err}`);
203
+ console.error('[AutoUpdater] Update was applied but manual restart is needed.');
204
+ }
205
+ }
206
+ /**
207
+ * Send a notification via Telegram (if configured).
208
+ * Falls back to console logging if Telegram is not available.
209
+ */
210
+ async notify(message) {
211
+ const formatted = `🔄 *Auto-Update*\n\n${message}`;
212
+ if (this.telegram) {
213
+ try {
214
+ const topicId = this.config.notificationTopicId || this.getAttentionTopicId();
215
+ if (topicId) {
216
+ await this.telegram.sendToTopic(topicId, formatted);
217
+ return;
218
+ }
219
+ }
220
+ catch (err) {
221
+ console.error(`[AutoUpdater] Telegram notification failed: ${err}`);
222
+ }
223
+ }
224
+ // Fallback: just log
225
+ console.log(`[AutoUpdater] Notification: ${message}`);
226
+ }
227
+ /**
228
+ * Get the Agent Attention topic ID from state (where infrastructure notifications go).
229
+ */
230
+ getAttentionTopicId() {
231
+ return this.state.get('agent-attention-topic') ?? 0;
232
+ }
233
+ /**
234
+ * Get the server port from the update checker config (for notification messages).
235
+ */
236
+ getPort() {
237
+ // The port is available on the UpdateChecker config but not exposed.
238
+ // Use a reasonable default — agents can find their port from config.
239
+ return 4040;
240
+ }
241
+ // ── State persistence ──────────────────────────────────────────────
242
+ loadState() {
243
+ try {
244
+ if (fs.existsSync(this.stateFile)) {
245
+ const data = JSON.parse(fs.readFileSync(this.stateFile, 'utf-8'));
246
+ this.lastCheck = data.lastCheck ?? null;
247
+ this.lastApply = data.lastApply ?? null;
248
+ this.lastError = data.lastError ?? null;
249
+ this.pendingUpdate = data.pendingUpdate ?? null;
250
+ }
251
+ }
252
+ catch {
253
+ // Start fresh if state is corrupted
254
+ }
255
+ }
256
+ saveState() {
257
+ const dir = path.dirname(this.stateFile);
258
+ fs.mkdirSync(dir, { recursive: true });
259
+ const data = {
260
+ lastCheck: this.lastCheck,
261
+ lastApply: this.lastApply,
262
+ lastError: this.lastError,
263
+ pendingUpdate: this.pendingUpdate,
264
+ savedAt: new Date().toISOString(),
265
+ };
266
+ // Atomic write
267
+ const tmpPath = this.stateFile + `.${process.pid}.tmp`;
268
+ try {
269
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
270
+ fs.renameSync(tmpPath, this.stateFile);
271
+ }
272
+ catch {
273
+ try {
274
+ fs.unlinkSync(tmpPath);
275
+ }
276
+ catch { /* ignore */ }
277
+ }
278
+ }
279
+ }
280
+ //# sourceMappingURL=AutoUpdater.js.map
@@ -127,24 +127,23 @@ export class SessionManager extends EventEmitter {
127
127
  if (this.tmuxSessionExists(tmuxSession)) {
128
128
  throw new Error(`tmux session "${tmuxSession}" already exists`);
129
129
  }
130
- // Build Claude CLI arguments via bash wrapper.
131
- // Must unset CLAUDECODE to prevent "cannot be launched inside another Claude Code session"
132
- // error when instar itself runs inside Claude Code.
130
+ // Build Claude CLI arguments no shell intermediary.
131
+ // tmux new-session executes the command directly (no bash -c needed)
132
+ // when given as separate arguments after the session options.
133
+ // Use -e CLAUDECODE= to unset the CLAUDECODE env var in spawned sessions,
134
+ // preventing nested Claude Code detection when instar runs inside Claude Code.
133
135
  const claudeArgs = ['--dangerously-skip-permissions'];
134
136
  if (options.model) {
135
137
  claudeArgs.push('--model', options.model);
136
138
  }
137
139
  claudeArgs.push('-p', options.prompt);
138
- const claudeCmd = [this.config.claudePath, ...claudeArgs]
139
- .map(a => a.replace(/'/g, "'\\''"))
140
- .map(a => `'${a}'`)
141
- .join(' ');
142
140
  try {
143
141
  execFileSync(this.config.tmuxPath, [
144
142
  'new-session', '-d',
145
143
  '-s', tmuxSession,
146
144
  '-c', this.config.projectDir,
147
- 'bash', '-c', `unset CLAUDECODE; exec ${claudeCmd}`,
145
+ '-e', 'CLAUDECODE=',
146
+ this.config.claudePath, ...claudeArgs,
148
147
  ], { encoding: 'utf-8' });
149
148
  }
150
149
  catch (err) {
@@ -385,14 +385,14 @@ Types: \`bug\`, \`feature\`, \`improvement\`, \`question\`
385
385
 
386
386
  ### The Feedback Loop — Rising Tide
387
387
 
388
- Instar has a built-in feedback loop that connects all agents to the development team (Dawn). When you submit feedback, it gets forwarded upstream. When a fix ships, the update-check job detects it and auto-applies. Every agent's bug report lifts all ships.
388
+ Instar has a built-in feedback loop that connects all agents to the development team (Dawn). When you submit feedback, it gets forwarded upstream. When a fix ships, the built-in auto-updater detects it, applies it, and restarts the server. Every agent's bug report lifts all ships.
389
389
 
390
390
  **The cycle:**
391
391
  1. You encounter an issue → submit feedback
392
392
  2. Feedback forwards upstream to Dawn
393
393
  3. Dawn fixes and publishes a new version
394
- 4. Your update-check job detects it within 30 minutes
395
- 5. Update auto-applies, hooks refresh, you get the fix
394
+ 4. The auto-updater detects it within 30 minutes
395
+ 5. Update auto-applies, hooks refresh, server restarts, you get the fix
396
396
 
397
397
  **User feedback matters too.** When your user says "this isn't working" or "I wish I could..." — that's feedback. Categorize it and submit it the same way.
398
398
 
@@ -14,6 +14,7 @@ import type { RelationshipManager } from '../core/RelationshipManager.js';
14
14
  import type { FeedbackManager } from '../core/FeedbackManager.js';
15
15
  import type { DispatchManager } from '../core/DispatchManager.js';
16
16
  import type { UpdateChecker } from '../core/UpdateChecker.js';
17
+ import type { AutoUpdater } from '../core/AutoUpdater.js';
17
18
  import type { QuotaTracker } from '../monitoring/QuotaTracker.js';
18
19
  import type { TelegraphService } from '../publishing/TelegraphService.js';
19
20
  import type { PrivateViewer } from '../publishing/PrivateViewer.js';
@@ -34,6 +35,7 @@ export declare class AgentServer {
34
35
  feedback?: FeedbackManager;
35
36
  dispatches?: DispatchManager;
36
37
  updateChecker?: UpdateChecker;
38
+ autoUpdater?: AutoUpdater;
37
39
  quotaTracker?: QuotaTracker;
38
40
  publisher?: TelegraphService;
39
41
  viewer?: PrivateViewer;
@@ -32,6 +32,7 @@ export class AgentServer {
32
32
  feedback: options.feedback ?? null,
33
33
  dispatches: options.dispatches ?? null,
34
34
  updateChecker: options.updateChecker ?? null,
35
+ autoUpdater: options.autoUpdater ?? null,
35
36
  quotaTracker: options.quotaTracker ?? null,
36
37
  publisher: options.publisher ?? null,
37
38
  viewer: options.viewer ?? null,
@@ -14,6 +14,7 @@ import type { RelationshipManager } from '../core/RelationshipManager.js';
14
14
  import type { FeedbackManager } from '../core/FeedbackManager.js';
15
15
  import type { DispatchManager } from '../core/DispatchManager.js';
16
16
  import type { UpdateChecker } from '../core/UpdateChecker.js';
17
+ import type { AutoUpdater } from '../core/AutoUpdater.js';
17
18
  import type { QuotaTracker } from '../monitoring/QuotaTracker.js';
18
19
  import type { TelegraphService } from '../publishing/TelegraphService.js';
19
20
  import type { PrivateViewer } from '../publishing/PrivateViewer.js';
@@ -29,6 +30,7 @@ export interface RouteContext {
29
30
  feedback: FeedbackManager | null;
30
31
  dispatches: DispatchManager | null;
31
32
  updateChecker: UpdateChecker | null;
33
+ autoUpdater: AutoUpdater | null;
32
34
  quotaTracker: QuotaTracker | null;
33
35
  publisher: TelegraphService | null;
34
36
  viewer: PrivateViewer | null;
@@ -476,6 +476,34 @@ export function createRoutes(ctx) {
476
476
  }
477
477
  res.json({ topics: ctx.telegram.getAllTopicMappings() });
478
478
  });
479
+ router.post('/telegram/topics', async (req, res) => {
480
+ if (!ctx.telegram) {
481
+ res.status(503).json({ error: 'Telegram not configured' });
482
+ return;
483
+ }
484
+ const { name, color } = req.body;
485
+ if (!name || typeof name !== 'string' || name.trim().length < 1) {
486
+ res.status(400).json({ error: '"name" is required (non-empty string)' });
487
+ return;
488
+ }
489
+ if (name.length > 128) {
490
+ res.status(400).json({ error: '"name" must be 128 characters or fewer' });
491
+ return;
492
+ }
493
+ // Color is optional — defaults to green (9367192)
494
+ const iconColor = typeof color === 'number' ? color : 9367192;
495
+ try {
496
+ const topic = await ctx.telegram.createForumTopic(name.trim(), iconColor);
497
+ res.status(201).json({
498
+ topicId: topic.topicId,
499
+ name: name.trim(),
500
+ created: true,
501
+ });
502
+ }
503
+ catch (err) {
504
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
505
+ }
506
+ });
479
507
  router.post('/telegram/reply/:topicId', async (req, res) => {
480
508
  if (!ctx.telegram) {
481
509
  res.status(503).json({ error: 'Telegram not configured' });
@@ -870,6 +898,14 @@ export function createRoutes(ctx) {
870
898
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
871
899
  }
872
900
  });
901
+ // ── Auto-Updater ────────────────────────────────────────────────
902
+ router.get('/updates/auto', (_req, res) => {
903
+ if (!ctx.autoUpdater) {
904
+ res.status(503).json({ error: 'Auto-updater not configured' });
905
+ return;
906
+ }
907
+ res.json(ctx.autoUpdater.getStatus());
908
+ });
873
909
  // ── Dispatches ───────────────────────────────────────────────────
874
910
  router.get('/dispatches', async (_req, res) => {
875
911
  if (!ctx.dispatches) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.7.22",
3
+ "version": "0.7.24",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",