mixdog 1.0.5 → 1.0.7
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.
- package/README.md +206 -475
- package/package.json +11 -4
- package/src/defaults/skills/setup/references/actions.md +26 -6
- package/src/defaults/skills/setup/references/surfaces.md +8 -3
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +16 -7
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +0 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +0 -2
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +24 -2
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +14 -1
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +3 -45
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +1 -1
- package/src/runtime/channels/lib/scheduler.mjs +15 -56
- package/src/runtime/channels/lib/worker-main.mjs +2 -9
- package/src/runtime/shared/config.mjs +66 -1
- package/src/runtime/shared/llm/usage-ledger-quota.mjs +13 -2
- package/src/runtime/shared/path-executable.mjs +58 -0
- package/src/session-runtime/boot/apis.mjs +6 -2
- package/src/session-runtime/boot/begin.mjs +0 -2
- package/src/session-runtime/boot/providers.mjs +3 -0
- package/src/session-runtime/boot/tools.mjs +0 -1
- package/src/session-runtime/config-lifecycle.mjs +11 -0
- package/src/session-runtime/internal-tool-executor.mjs +0 -2
- package/src/session-runtime/runtime-feature-gates.mjs +2 -3
- package/src/session-runtime/schedule-session-run.mjs +0 -1
- package/src/session-runtime/services/channel-admin.mjs +8 -26
- package/src/session-runtime/settings-compaction-api.mjs +5 -1
- package/src/session-runtime/settings-system-api.mjs +2 -20
- package/src/session-runtime/setup-tool/executor.mjs +61 -3
- package/src/session-runtime/setup-tool/extended-actions.mjs +31 -6
- package/src/session-runtime/setup-tool/settings-contract.mjs +27 -3
- package/src/session-runtime/setup-tool/tool-defs.mjs +35 -5
- package/src/session-runtime/webhook-session-run.mjs +0 -2
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +13 -3
- package/src/standalone/daemon-stored-session-views.mjs +4 -0
- package/src/standalone/session-protocol.mjs +0 -1
- package/src/standalone/session-service/session-calls/session-views.mjs +10 -4
- package/src/standalone/session-service/stored-reader.mjs +20 -2
- package/src/standalone/session-service/viewers.mjs +3 -3
- package/src/standalone/session-service.mjs +3 -1
- package/src/tui/App.jsx +6 -1
- package/src/tui/app/create-app-pickers.mjs +1 -0
- package/src/tui/app/doctor.mjs +6 -14
- package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +8 -3
- package/src/tui/app/settings-picker/settings-rows.mjs +5 -1
- package/src/tui/app/use-ui-open-request.mjs +13 -3
- package/src/tui/dist/index.mjs +41 -9
- package/src/tui/session/session-api/settings.mjs +0 -6
|
@@ -97,14 +97,12 @@ function releaseSchedulerRuntime(scheduler) {
|
|
|
97
97
|
class Scheduler {
|
|
98
98
|
nonInteractive;
|
|
99
99
|
interactive;
|
|
100
|
-
channelId;
|
|
101
100
|
promptsDir;
|
|
102
101
|
tickTimer = null;
|
|
103
102
|
lastFired = /* @__PURE__ */ new Map();
|
|
104
103
|
// name -> "YYYY-MM-DDTHH:MM"
|
|
105
104
|
running = /* @__PURE__ */ new Set();
|
|
106
105
|
injectFn = null;
|
|
107
|
-
sendFn = null;
|
|
108
106
|
injectReadyFn = null;
|
|
109
107
|
pendingCheck = null;
|
|
110
108
|
// Injected by the host: (schedule, { prompt }) => Promise<{ result }>.
|
|
@@ -118,13 +116,9 @@ class Scheduler {
|
|
|
118
116
|
// name -> node-cron ScheduledTask for cron-expression entries
|
|
119
117
|
oneShotTimers = /* @__PURE__ */ new Map();
|
|
120
118
|
// name -> setTimeout handle for when_at one-shot entries
|
|
121
|
-
|
|
122
|
-
// `channelId` is the single resolved main-channel id used when a schedule's
|
|
123
|
-
// `channel` flag is set (post-to-channel); absent flag → inject into session.
|
|
124
|
-
constructor(nonInteractive, interactive, channelId) {
|
|
119
|
+
constructor(nonInteractive, interactive) {
|
|
125
120
|
this.nonInteractive = nonInteractive.filter((s) => s.enabled !== false);
|
|
126
121
|
this.interactive = interactive.filter((s) => s.enabled !== false);
|
|
127
|
-
this.channelId = channelId ?? '';
|
|
128
122
|
this.promptsDir = join(DATA_DIR, 'prompts');
|
|
129
123
|
for (const s of [...this.nonInteractive, ...this.interactive]) {
|
|
130
124
|
if (s.lastFiredAt) this.lastFired.set(s.name, new Date(s.lastFiredAt).toISOString());
|
|
@@ -134,9 +128,6 @@ class Scheduler {
|
|
|
134
128
|
setInjectHandler(fn) {
|
|
135
129
|
this.injectFn = fn;
|
|
136
130
|
}
|
|
137
|
-
setSendHandler(fn) {
|
|
138
|
-
this.sendFn = fn;
|
|
139
|
-
}
|
|
140
131
|
/** Probe for a live Lead bridge seat. When set, the interactive
|
|
141
132
|
* inject only fires while it reports true; otherwise the visible-session
|
|
142
133
|
* fallback keeps the fire from being lost. Unset → inject when wired. */
|
|
@@ -561,10 +552,9 @@ ${prompt}`;
|
|
|
561
552
|
releaseSchedulerRuntime(this);
|
|
562
553
|
this.start();
|
|
563
554
|
}
|
|
564
|
-
reloadConfig(nonInteractive, interactive,
|
|
555
|
+
reloadConfig(nonInteractive, interactive, options = {}) {
|
|
565
556
|
this.nonInteractive = nonInteractive.filter((s) => s.enabled !== false);
|
|
566
557
|
this.interactive = interactive.filter((s) => s.enabled !== false);
|
|
567
|
-
this.channelId = channelId ?? '';
|
|
568
558
|
this.promptsDir = join(DATA_DIR, 'prompts');
|
|
569
559
|
// Defer/skip state is persisted (deferred_until / skipped_until) and
|
|
570
560
|
// re-read from the reloaded rows, so a reload no longer drops it.
|
|
@@ -629,14 +619,10 @@ ${prompt}`;
|
|
|
629
619
|
this.notifyFailure(schedule, 'prompt not found — fire skipped');
|
|
630
620
|
return false;
|
|
631
621
|
}
|
|
632
|
-
|
|
633
|
-
// (falling back to the resolved main channel). target 'session' → no
|
|
634
|
-
// channel relay; the visible session run IS the surface.
|
|
635
|
-
const channelId = schedule.target === 'channel' ? this.resolveChannel(schedule.channelId) : '';
|
|
636
|
-
return await this.fireTimedPrompt(schedule, type, prompt, channelId, opts);
|
|
622
|
+
return await this.fireTimedPrompt(schedule, type, prompt, opts);
|
|
637
623
|
}
|
|
638
624
|
/** Fire a timed schedule with the given prompt content */
|
|
639
|
-
async fireTimedPrompt(schedule, type, prompt,
|
|
625
|
+
async fireTimedPrompt(schedule, type, prompt, { awaitDispatch = false } = {}) {
|
|
640
626
|
logSchedule(`firing ${schedule.name} (${type})\n`);
|
|
641
627
|
if (this.running.has(schedule.name)) {
|
|
642
628
|
// Not silent anymore: overlap skips were invisible and read as the
|
|
@@ -644,7 +630,7 @@ ${prompt}`;
|
|
|
644
630
|
logSchedule(`${schedule.name}: skipped — previous run still in progress\n`);
|
|
645
631
|
return false;
|
|
646
632
|
}
|
|
647
|
-
if (type === 'interactive' && this.injectIntoLead(schedule, type, prompt
|
|
633
|
+
if (type === 'interactive' && this.injectIntoLead(schedule, type, prompt)) return true;
|
|
648
634
|
this.running.add(schedule.name);
|
|
649
635
|
const presetId = schedule.model;
|
|
650
636
|
if (!presetId) {
|
|
@@ -657,7 +643,7 @@ ${prompt}`;
|
|
|
657
643
|
// and swallow async failures. A one-shot (awaitDispatch) instead awaits
|
|
658
644
|
// the dispatch so a rejected run propagates and the caller leaves the
|
|
659
645
|
// entry pending for retry instead of retiring it.
|
|
660
|
-
const dispatch = this.dispatchScheduleRun(schedule, prompt,
|
|
646
|
+
const dispatch = this.dispatchScheduleRun(schedule, prompt, awaitDispatch);
|
|
661
647
|
if (awaitDispatch) return await dispatch;
|
|
662
648
|
return true;
|
|
663
649
|
}
|
|
@@ -666,11 +652,11 @@ ${prompt}`;
|
|
|
666
652
|
* exactly like inbound channel messages — enqueue counts as the fire.
|
|
667
653
|
* False when no live Lead seat is attached or the inject fails, so the
|
|
668
654
|
* visible-session run remains the fallback and a fire is never lost. */
|
|
669
|
-
injectIntoLead(schedule, type, prompt
|
|
655
|
+
injectIntoLead(schedule, type, prompt) {
|
|
670
656
|
if (!this.injectFn || !this.injectReady()) return false;
|
|
671
657
|
try {
|
|
672
658
|
const wrapped = this.wrapPrompt(schedule.name, prompt, type);
|
|
673
|
-
this.injectFn(
|
|
659
|
+
this.injectFn('', `schedule:${schedule.name}`, ' ', { type: 'schedule', instruction: wrapped });
|
|
674
660
|
logSchedule(`${schedule.name}: injected into Lead session queue (interactive fire)\n`);
|
|
675
661
|
return true;
|
|
676
662
|
} catch (err) {
|
|
@@ -682,21 +668,16 @@ ${prompt}`;
|
|
|
682
668
|
}
|
|
683
669
|
/** Fires run as VISIBLE schedule sessions (desktop Recent / TUI resume)
|
|
684
670
|
* via runScheduleSession; the wrapped prompt keeps the schedule context
|
|
685
|
-
* header
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
dispatchScheduleRun(schedule, prompt,
|
|
671
|
+
* header. Resolves true on success; a failure rejects for awaitDispatch
|
|
672
|
+
* callers and resolves false (after a logged notice) for fire-and-forget
|
|
673
|
+
* cron fires. */
|
|
674
|
+
dispatchScheduleRun(schedule, prompt, awaitDispatch) {
|
|
689
675
|
const run = this.scheduleRunner
|
|
690
676
|
? this.scheduleRunner(schedule, { prompt })
|
|
691
677
|
: Promise.reject(new Error('schedule runner not configured'));
|
|
692
678
|
return run
|
|
693
679
|
.then(({ result }) => {
|
|
694
680
|
this.running.delete(schedule.name);
|
|
695
|
-
if (result && channelId && this.sendFn) {
|
|
696
|
-
this.sendFn(channelId, result).catch((err) =>
|
|
697
|
-
process.stderr.write(`mixdog scheduler: ${schedule.name} relay failed: ${err}\n`)
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
681
|
logSchedule(`${schedule.name} done\n`);
|
|
701
682
|
return true;
|
|
702
683
|
})
|
|
@@ -704,7 +685,7 @@ ${prompt}`;
|
|
|
704
685
|
this.running.delete(schedule.name);
|
|
705
686
|
logSchedule(`${schedule.name} LLM error: ${err.message}\n`);
|
|
706
687
|
// The cron fire-and-forget contract swallowed failures entirely;
|
|
707
|
-
//
|
|
688
|
+
// log them so a failed scheduled run is never
|
|
708
689
|
// silent (one-shots propagate via the awaitDispatch throw instead).
|
|
709
690
|
if (awaitDispatch) throw err;
|
|
710
691
|
this.notifyFailure(schedule, `run failed: ${err.message}`);
|
|
@@ -712,18 +693,9 @@ ${prompt}`;
|
|
|
712
693
|
});
|
|
713
694
|
}
|
|
714
695
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
715
|
-
/**
|
|
716
|
-
* channel) so scheduled fires never fail silently. Respects an explicit
|
|
717
|
-
* channel opt-out ("false" → resolveChannel yields "" → no notice). */
|
|
696
|
+
/** Failure notice: logged so scheduled fires never fail silently. */
|
|
718
697
|
notifyFailure(schedule, reason) {
|
|
719
|
-
|
|
720
|
-
if (!this.sendFn) return;
|
|
721
|
-
const target = this.resolveChannel(schedule?.channelId);
|
|
722
|
-
if (!target) return;
|
|
723
|
-
Promise.resolve(this.sendFn(target, `schedule "${schedule?.name}": ${reason}`)).catch(() => {});
|
|
724
|
-
} catch {
|
|
725
|
-
/* best-effort */
|
|
726
|
-
}
|
|
698
|
+
logSchedule(`schedule "${schedule?.name}": ${reason}\n`);
|
|
727
699
|
}
|
|
728
700
|
/** Resolve prompt: inline text from the row, with a prompts-dir file
|
|
729
701
|
* fallback for legacy `<name>.md` references. */
|
|
@@ -736,18 +708,5 @@ ${prompt}`;
|
|
|
736
708
|
const full = isAbsolute(nameOrPath) ? nameOrPath : join(this.promptsDir, nameOrPath);
|
|
737
709
|
return tryRead(full);
|
|
738
710
|
}
|
|
739
|
-
/** Resolve a schedule's channel flag to a channel id (pre-refactor
|
|
740
|
-
* semantics): absent/empty/"false" → "" (inject into session); a
|
|
741
|
-
* pure-digit / snowflake value is honored verbatim as an explicit id
|
|
742
|
-
* override; any other value — including legacy labels like "main" —
|
|
743
|
-
* resolves to the configured main channel. */
|
|
744
|
-
resolveChannel(flag) {
|
|
745
|
-
if (flag == null) return this.channelId ?? '';
|
|
746
|
-
const v = String(flag).trim();
|
|
747
|
-
if (v.toLowerCase() === 'false') return '';
|
|
748
|
-
if (/^-?\d+$/.test(v)) return v;
|
|
749
|
-
// empty or a legacy label ("main") → configured main channel
|
|
750
|
-
return this.channelId ?? '';
|
|
751
|
-
}
|
|
752
711
|
}
|
|
753
712
|
export { Scheduler };
|
|
@@ -131,9 +131,7 @@ const statusState = new JsonStateFile(STATUS_FILE, {});
|
|
|
131
131
|
statusState.ensure();
|
|
132
132
|
const scheduler = new Scheduler(
|
|
133
133
|
config.nonInteractive ?? [],
|
|
134
|
-
config.interactive ?? []
|
|
135
|
-
// Single resolved main-channel id used for the schedule `channel` flag.
|
|
136
|
-
config.channelId
|
|
134
|
+
config.interactive ?? []
|
|
137
135
|
);
|
|
138
136
|
// Schedules fire as visible sessions; the session runner belongs to the
|
|
139
137
|
// session-runtime layer, so this worker entry injects it (lazy, like webhooks).
|
|
@@ -220,16 +218,12 @@ scheduler.setInjectHandler((channelId, name, content, options) => {
|
|
|
220
218
|
// actually holds the live bridge seat; otherwise the scheduler falls back to
|
|
221
219
|
// the visible-session run so the fire is never lost.
|
|
222
220
|
scheduler.setInjectReadyCheck(() => bridgeRuntimeConnected && currentOwnerState().owned);
|
|
223
|
-
scheduler.setSendHandler(async () => {
|
|
224
|
-
// Channel delivery is retired: schedule results surface in the app session
|
|
225
|
-
// (parent notify / Automations row) only.
|
|
226
|
-
});
|
|
227
221
|
function wireWebhookHandlers() {
|
|
228
222
|
if (!webhookServer) return;
|
|
229
223
|
webhookServer.setEventPipeline(eventPipeline);
|
|
230
224
|
// Webhook fires run as sessions (schedules parity); the Automations
|
|
231
225
|
// session row is the only surface (channel relay retired).
|
|
232
|
-
webhookServer.setBridgeDispatch(async ({ prompt, model, cwd, workflow, attachments,
|
|
226
|
+
webhookServer.setBridgeDispatch(async ({ prompt, model, cwd, workflow, attachments, context, signal }) => {
|
|
233
227
|
const { runWebhookSession } = await import('../../../session-runtime/webhook-session-run.mjs');
|
|
234
228
|
const run = await runWebhookSession({
|
|
235
229
|
name: context?.endpoint || 'webhook',
|
|
@@ -237,7 +231,6 @@ function wireWebhookHandlers() {
|
|
|
237
231
|
cwd: cwd || null,
|
|
238
232
|
workflow: workflow || null,
|
|
239
233
|
attachments: attachments || null,
|
|
240
|
-
delivery: delivery || null,
|
|
241
234
|
// Dispatch-timeout cancellation only works if the signal reaches the run.
|
|
242
235
|
signal: signal || null,
|
|
243
236
|
prompt,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Unified config reader/writer.
|
|
3
3
|
* Single file: mixdog-config.json with sections such as channels, agent, and memory.
|
|
4
4
|
*/
|
|
5
|
-
import { readFileSync, statSync, mkdirSync, existsSync } from 'node:fs';
|
|
5
|
+
import { readFileSync, statSync, mkdirSync, existsSync, watch as fsWatch } from 'node:fs';
|
|
6
6
|
import { join, dirname } from 'node:path';
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
8
|
import { resolvePluginData } from './plugin-paths.mjs';
|
|
@@ -315,8 +315,72 @@ function readAllForRmW() {
|
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
// Config-change notification. In-process writes notify directly after the file
|
|
319
|
+
// is persisted; writes from other processes arrive through one lazily started
|
|
320
|
+
// directory watch (the atomic rename replaces the file, which would orphan a
|
|
321
|
+
// file-level watch on POSIX). Bursts coalesce into one listener pass.
|
|
322
|
+
const CONFIG_CHANGE_DEBOUNCE_MS = 100;
|
|
323
|
+
const _configChangeListeners = new Set();
|
|
324
|
+
let _configChangeTimer = null;
|
|
325
|
+
let _configWatcher = null;
|
|
326
|
+
let _configWatchDir = '';
|
|
327
|
+
|
|
328
|
+
function stopConfigWatcher() {
|
|
329
|
+
try {
|
|
330
|
+
_configWatcher?.close();
|
|
331
|
+
} catch {}
|
|
332
|
+
_configWatcher = null;
|
|
333
|
+
_configWatchDir = '';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function notifyConfigChanged() {
|
|
337
|
+
if (!_configChangeListeners.size) return;
|
|
338
|
+
if (_configChangeTimer) clearTimeout(_configChangeTimer);
|
|
339
|
+
_configChangeTimer = setTimeout(() => {
|
|
340
|
+
_configChangeTimer = null;
|
|
341
|
+
invalidateConfigReadCache();
|
|
342
|
+
for (const listener of [..._configChangeListeners]) {
|
|
343
|
+
try {
|
|
344
|
+
listener();
|
|
345
|
+
} catch {}
|
|
346
|
+
}
|
|
347
|
+
}, CONFIG_CHANGE_DEBOUNCE_MS);
|
|
348
|
+
_configChangeTimer.unref?.();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function ensureConfigWatcher() {
|
|
352
|
+
const dir = dirname(configPath());
|
|
353
|
+
if (_configWatcher && _configWatchDir === dir) return;
|
|
354
|
+
stopConfigWatcher();
|
|
355
|
+
try {
|
|
356
|
+
_configWatcher = fsWatch(dir, (_event, filename) => {
|
|
357
|
+
if (!filename || String(filename) === 'mixdog-config.json') notifyConfigChanged();
|
|
358
|
+
});
|
|
359
|
+
_configWatcher.unref?.();
|
|
360
|
+
_configWatcher.on?.('error', stopConfigWatcher);
|
|
361
|
+
_configWatchDir = dir;
|
|
362
|
+
} catch {
|
|
363
|
+
_configWatcher = null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Subscribe to persisted config changes (this process or another). Returns unsubscribe. */
|
|
368
|
+
export function subscribeConfigChange(listener) {
|
|
369
|
+
_configChangeListeners.add(listener);
|
|
370
|
+
ensureConfigWatcher();
|
|
371
|
+
return () => {
|
|
372
|
+
_configChangeListeners.delete(listener);
|
|
373
|
+
if (!_configChangeListeners.size) {
|
|
374
|
+
if (_configChangeTimer) clearTimeout(_configChangeTimer);
|
|
375
|
+
_configChangeTimer = null;
|
|
376
|
+
stopConfigWatcher();
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
318
381
|
function writeAll(data) {
|
|
319
382
|
writeConfigFile(canonicalizeUnifiedConfig(data));
|
|
383
|
+
notifyConfigChanged();
|
|
320
384
|
}
|
|
321
385
|
|
|
322
386
|
// Serialize a read-modify-write under the same file lock. Concurrent
|
|
@@ -390,6 +454,7 @@ async function writeConfigFileAsync(data) {
|
|
|
390
454
|
|
|
391
455
|
async function writeAllAsync(data) {
|
|
392
456
|
await writeConfigFileAsync(canonicalizeUnifiedConfig(data));
|
|
457
|
+
notifyConfigChanged();
|
|
393
458
|
}
|
|
394
459
|
|
|
395
460
|
function withConfigLockAsync(fn) {
|
|
@@ -209,11 +209,22 @@ function quotaInstances(rows, label, now) {
|
|
|
209
209
|
paced: false,
|
|
210
210
|
};
|
|
211
211
|
});
|
|
212
|
+
// After a reset, early or on time, the meter reads zero until the next
|
|
213
|
+
// request: that last run is the window open now, not a gap between windows.
|
|
214
|
+
const latest = instances.at(-1);
|
|
215
|
+
if (
|
|
216
|
+
latest?.idle &&
|
|
217
|
+
instances.some((instance) => !instance.idle) &&
|
|
218
|
+
(latest.resetAt == null || latest.resetAt > now)
|
|
219
|
+
) {
|
|
220
|
+
latest.idle = false;
|
|
221
|
+
}
|
|
212
222
|
const active = instances.filter((instance) => !instance.idle);
|
|
213
223
|
let previousEnd = -Infinity;
|
|
214
|
-
active.forEach((instance
|
|
224
|
+
active.forEach((instance) => {
|
|
215
225
|
const first = instance.rows[0];
|
|
216
|
-
|
|
226
|
+
// Any later reading, even an idle one after an early reset, closes it.
|
|
227
|
+
const next = instances[instances.indexOf(instance) + 1];
|
|
217
228
|
// A window's clock started at its opening: never after its first reading,
|
|
218
229
|
// never inside the window before it.
|
|
219
230
|
const opened = quotaWindowOpened(label, instance.resetAt);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// PATH lookups with no other runtime dependency, so a bundle that only needs
|
|
2
|
+
// "is this executable installed?" (the desktop service's feature gates) does
|
|
3
|
+
// not pull in the git worker or the rest of runtime-capabilities.
|
|
4
|
+
import { accessSync, constants as fsConstants, statSync } from 'node:fs';
|
|
5
|
+
import { delimiter as pathDelimiter, join as pathJoin } from 'node:path';
|
|
6
|
+
|
|
7
|
+
function _pathDirectory(value) {
|
|
8
|
+
const text = String(value || '').trim();
|
|
9
|
+
return text.length >= 2 && text.startsWith('"') && text.endsWith('"') ? text.slice(1, -1) : text;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function _executableNames(name, platform) {
|
|
13
|
+
const text = String(name || '').trim();
|
|
14
|
+
if (!text) return [];
|
|
15
|
+
if (platform !== 'win32' || /\.(?:exe|cmd|bat|com)$/i.test(text)) return [text];
|
|
16
|
+
return [text, `${text}.exe`, `${text}.cmd`, `${text}.bat`, `${text}.com`];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function findPathExecutable(
|
|
20
|
+
name,
|
|
21
|
+
{ pathValue = process.env.PATH || '', platform = process.platform, maxDirectories = 64 } = {}
|
|
22
|
+
) {
|
|
23
|
+
const names = _executableNames(name, platform);
|
|
24
|
+
if (!names.length) return null;
|
|
25
|
+
const seenDirectories = new Set();
|
|
26
|
+
for (const rawDirectory of String(pathValue).split(pathDelimiter)) {
|
|
27
|
+
const directory = _pathDirectory(rawDirectory);
|
|
28
|
+
if (!directory) continue;
|
|
29
|
+
const key = platform === 'win32' ? directory.toLowerCase() : directory;
|
|
30
|
+
if (seenDirectories.has(key)) continue;
|
|
31
|
+
seenDirectories.add(key);
|
|
32
|
+
if (seenDirectories.size > maxDirectories) break;
|
|
33
|
+
for (const executable of names) {
|
|
34
|
+
const file = pathJoin(directory, executable);
|
|
35
|
+
try {
|
|
36
|
+
if (!statSync(file).isFile()) continue;
|
|
37
|
+
accessSync(file, platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK);
|
|
38
|
+
return `${executable} (${directory.replace(/\\/g, '/')})`;
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Whether a `git` executable is on PATH. The tool surface asks on every
|
|
46
|
+
// policy refresh, so the PATH walk is memoized briefly; the short window still
|
|
47
|
+
// lets a session opened after a Git install pick the tool up.
|
|
48
|
+
const GIT_PRESENCE_TTL_MS = 30_000;
|
|
49
|
+
let _gitPresence = null;
|
|
50
|
+
export function gitExecutablePresent({ pathValue = process.env.PATH || '', platform = process.platform } = {}) {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
if (_gitPresence && _gitPresence.pathValue === pathValue && now - _gitPresence.at < GIT_PRESENCE_TTL_MS) {
|
|
53
|
+
return _gitPresence.present;
|
|
54
|
+
}
|
|
55
|
+
const present = Boolean(findPathExecutable('git', { pathValue, platform }));
|
|
56
|
+
_gitPresence = { pathValue, at: now, present };
|
|
57
|
+
return present;
|
|
58
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
normalizeSystemShellCommand,
|
|
21
21
|
normalizeAutoClearConfig,
|
|
22
22
|
autoClearIdleMsForProvider,
|
|
23
|
+
resolveAutoClearIdleMs,
|
|
23
24
|
autoClearProviderDefaults,
|
|
24
25
|
normalizeCompactionConfig,
|
|
25
26
|
setModuleEnabledInConfig,
|
|
@@ -75,7 +76,10 @@ export function wireApis(boot) {
|
|
|
75
76
|
});
|
|
76
77
|
boot.lifecycleApi = lifecycleApiFor(boot);
|
|
77
78
|
boot.resourceApi = resourceApiFor(boot);
|
|
78
|
-
boot.disposeGlobalExtensionSubscription = () =>
|
|
79
|
+
boot.disposeGlobalExtensionSubscription = () => {
|
|
80
|
+
boot.disposeConfigSync?.();
|
|
81
|
+
boot.resourceApi.disposeGlobalExtensionSubscription?.();
|
|
82
|
+
};
|
|
79
83
|
boot.modelRouteApi = modelRouteApiFor(boot);
|
|
80
84
|
boot.workflowAgentsApi = workflowAgentsApiFor(boot);
|
|
81
85
|
boot.sessionTurnApi = sessionTurnApiFor(boot);
|
|
@@ -207,6 +211,7 @@ function settingsApiFor(boot) {
|
|
|
207
211
|
hasOwn,
|
|
208
212
|
normalizeAutoClearConfig,
|
|
209
213
|
autoClearIdleMsForProvider,
|
|
214
|
+
resolveAutoClearIdleMs,
|
|
210
215
|
autoClearProviderDefaults,
|
|
211
216
|
normalizeCompactionConfig,
|
|
212
217
|
normalizeSystemShellConfig,
|
|
@@ -229,7 +234,6 @@ function settingsApiFor(boot) {
|
|
|
229
234
|
tidyToolEnabledFn: boot.tidyToolEnabledFn,
|
|
230
235
|
localProviderEnabledFn: boot.localProviderEnabledFn,
|
|
231
236
|
webSearchEnabled: boot.webSearchEnabled,
|
|
232
|
-
channelsEnabled: boot.channelsEnabled,
|
|
233
237
|
autoUpdateEnabled: selfUpdate.autoUpdateEnabled,
|
|
234
238
|
getUpdateCheckState: () => selfUpdate.getCheckState(),
|
|
235
239
|
getUpdateProcessState: () => selfUpdate.getProcessState(),
|
|
@@ -86,7 +86,6 @@ export async function loadModules(boot) {
|
|
|
86
86
|
localProviderEnabledFn,
|
|
87
87
|
mediaToolEnabledFn,
|
|
88
88
|
tidyToolEnabledFn,
|
|
89
|
-
channelsEnabled,
|
|
90
89
|
featureDisallowedTools,
|
|
91
90
|
} = createRuntimeFeatureGates({
|
|
92
91
|
getConfig: () => rt.config,
|
|
@@ -114,7 +113,6 @@ export async function loadModules(boot) {
|
|
|
114
113
|
localProviderEnabledFn,
|
|
115
114
|
mediaToolEnabledFn,
|
|
116
115
|
tidyToolEnabledFn,
|
|
117
|
-
channelsEnabled,
|
|
118
116
|
featureDisallowedTools,
|
|
119
117
|
getMemoryModule,
|
|
120
118
|
getWebSearchModule,
|
|
@@ -71,9 +71,11 @@ function wireConfigLifecycle(boot) {
|
|
|
71
71
|
LAZY_SECRET_PROVIDERS,
|
|
72
72
|
clean,
|
|
73
73
|
resolve,
|
|
74
|
+
onConfigReloaded: () => boot.invalidateContextStatusCache?.(),
|
|
74
75
|
STANDALONE_DATA_DIR,
|
|
75
76
|
});
|
|
76
77
|
const {
|
|
78
|
+
disposeConfigSync,
|
|
77
79
|
getOutputStyleStatusCached,
|
|
78
80
|
invalidateOutputStyleStatusCache,
|
|
79
81
|
seedOutputStyleStatusCache,
|
|
@@ -89,6 +91,7 @@ function wireConfigLifecycle(boot) {
|
|
|
89
91
|
ensureConfigForRouteProvider,
|
|
90
92
|
} = lifecycle;
|
|
91
93
|
Object.assign(boot, {
|
|
94
|
+
disposeConfigSync,
|
|
92
95
|
getOutputStyleStatusCached,
|
|
93
96
|
invalidateOutputStyleStatusCache,
|
|
94
97
|
seedOutputStyleStatusCache,
|
|
@@ -232,7 +232,6 @@ function wireInternalTools(boot) {
|
|
|
232
232
|
officeToolsEnabled: boot.officeToolsEnabledFn,
|
|
233
233
|
mediaToolEnabled: boot.mediaToolEnabledFn,
|
|
234
234
|
tidyToolEnabled: boot.tidyToolEnabledFn,
|
|
235
|
-
channelsEnabled: boot.channelsEnabled,
|
|
236
235
|
getWebSearchModule: boot.getWebSearchModule,
|
|
237
236
|
getMemoryModule: boot.getMemoryModule,
|
|
238
237
|
getCodeGraphModule: boot.getCodeGraphModule,
|
|
@@ -66,6 +66,7 @@ export function createConfigLifecycle({
|
|
|
66
66
|
clean,
|
|
67
67
|
resolve,
|
|
68
68
|
performanceNow = () => performance.now(),
|
|
69
|
+
onConfigReloaded,
|
|
69
70
|
STANDALONE_DATA_DIR,
|
|
70
71
|
}) {
|
|
71
72
|
const outputStyleCache = createOutputStyleStatusCache({
|
|
@@ -160,7 +161,17 @@ export function createConfigLifecycle({
|
|
|
160
161
|
return ensureFullConfig();
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
// Settings persisted by any other runtime (or process) reach this runtime's
|
|
165
|
+
// in-memory config. reloadFullConfig keeps our own pending edits, and the
|
|
166
|
+
// shared layer coalesces bursts into one pass per subscriber.
|
|
167
|
+
const unsubscribeConfigChange = sharedCfgMod?.subscribeConfigChange?.(() => {
|
|
168
|
+
reloadFullConfig();
|
|
169
|
+
outputStyleCache.invalidateOutputStyleStatusCache();
|
|
170
|
+
onConfigReloaded?.();
|
|
171
|
+
});
|
|
172
|
+
|
|
163
173
|
return {
|
|
174
|
+
disposeConfigSync: () => unsubscribeConfigChange?.(),
|
|
164
175
|
// output-style cache
|
|
165
176
|
...outputStyleCache,
|
|
166
177
|
// adopt / save
|
|
@@ -22,7 +22,6 @@ export function createInternalToolExecutor({
|
|
|
22
22
|
officeToolsEnabled,
|
|
23
23
|
mediaToolEnabled,
|
|
24
24
|
tidyToolEnabled = () => true,
|
|
25
|
-
channelsEnabled,
|
|
26
25
|
getWebSearchModule,
|
|
27
26
|
getMemoryModule,
|
|
28
27
|
getCodeGraphModule,
|
|
@@ -68,7 +67,6 @@ export function createInternalToolExecutor({
|
|
|
68
67
|
if (callerCtx?.invocationSource === 'model-tool') guardModelCall(name);
|
|
69
68
|
if (Object.hasOwn(handlers, name)) return handlers[name](args, { name, callerCtx, callerCwd });
|
|
70
69
|
if (channels.isChannelTool(name)) {
|
|
71
|
-
if (!channelsEnabled()) throw new Error('channels are disabled in settings');
|
|
72
70
|
return await channels.execute(name, args || {});
|
|
73
71
|
}
|
|
74
72
|
throw new Error(`unknown standalone internal tool: ${name}`);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Key names mirror the dependency names of settings-api.mjs / the session-build
|
|
6
6
|
// deps, so the runtime can hand them straight through.
|
|
7
7
|
import { builtinFeatureActive, featureDisallowedToolsFor, localGitToolsActive } from '../runtime/agent/orchestrator/runtime-core/builtin-features.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import { recapEnabled } from '../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
|
|
9
9
|
import { browserBridgeAvailableSync } from '../runtime/browser-bridge/client.mjs';
|
|
10
10
|
import { computerBridgeAvailableSync } from '../runtime/computer-bridge/client.mjs';
|
|
11
11
|
|
|
@@ -17,12 +17,11 @@ export function createRuntimeFeatureGates({ getConfig, getToolProfile }) {
|
|
|
17
17
|
recapEnabledFn: () => recapEnabled(getConfig(), true),
|
|
18
18
|
memoryToolsEnabledFn: () => builtinFeatureActive(getConfig(), 'memory'),
|
|
19
19
|
webSearchEnabled: () => builtinFeatureActive(getConfig(), 'webSearch'),
|
|
20
|
-
gitToolsEnabledFn: () => localGitToolsActive(getConfig()
|
|
20
|
+
gitToolsEnabledFn: () => localGitToolsActive(getConfig()),
|
|
21
21
|
officeToolsEnabledFn: () => builtinFeatureActive(getConfig(), 'office'),
|
|
22
22
|
localProviderEnabledFn: () => builtinFeatureActive(getConfig(), 'localProvider'),
|
|
23
23
|
mediaToolEnabledFn: () => builtinFeatureActive(getConfig(), 'media'),
|
|
24
24
|
tidyToolEnabledFn: () => builtinFeatureActive(getConfig(), 'tidy'),
|
|
25
|
-
channelsEnabled: () => moduleEnabled(getConfig(), 'channels', true),
|
|
26
25
|
// Browser/Computer activate on live desktop-bridge presence, so the probe
|
|
27
26
|
// runs per call instead of being captured at boot.
|
|
28
27
|
featureDisallowedTools: () =>
|
|
@@ -48,7 +48,6 @@ export async function runScheduleSession(schedule, { config = null, prompt: prom
|
|
|
48
48
|
owner: 'user',
|
|
49
49
|
sourceType: 'schedule',
|
|
50
50
|
sourceName: schedule.name,
|
|
51
|
-
sourceDelivery: schedule.delivery || null,
|
|
52
51
|
...(cwd ? { cwd } : {}),
|
|
53
52
|
desktopSession: cwd
|
|
54
53
|
? { classification: 'project', projectPath: cwd }
|