@openacp/cli 2026.410.1 → 2026.410.2
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/dist/{channel-CKXNnTy4.d.ts → channel-CFMUPzvH.d.ts} +239 -21
- package/dist/cli.d.ts +21 -0
- package/dist/cli.js +1143 -32
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1999 -35
- package/dist/index.js +1014 -31
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -429,37 +429,46 @@ var init_plugin_registry = __esm({
|
|
|
429
429
|
this.registryPath = registryPath;
|
|
430
430
|
}
|
|
431
431
|
data = { installed: {} };
|
|
432
|
+
/** Return all installed plugins as a Map. */
|
|
432
433
|
list() {
|
|
433
434
|
return new Map(Object.entries(this.data.installed));
|
|
434
435
|
}
|
|
436
|
+
/** Look up a plugin by name. Returns undefined if not installed. */
|
|
435
437
|
get(name) {
|
|
436
438
|
return this.data.installed[name];
|
|
437
439
|
}
|
|
440
|
+
/** Record a newly installed plugin. Timestamps are set automatically. */
|
|
438
441
|
register(name, entry) {
|
|
439
442
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
440
443
|
this.data.installed[name] = { ...entry, installedAt: now, updatedAt: now };
|
|
441
444
|
}
|
|
445
|
+
/** Remove a plugin from the registry. */
|
|
442
446
|
remove(name) {
|
|
443
447
|
delete this.data.installed[name];
|
|
444
448
|
}
|
|
449
|
+
/** Enable or disable a plugin. Disabled plugins are skipped at boot. */
|
|
445
450
|
setEnabled(name, enabled) {
|
|
446
451
|
const entry = this.data.installed[name];
|
|
447
452
|
if (!entry) return;
|
|
448
453
|
entry.enabled = enabled;
|
|
449
454
|
entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
450
455
|
}
|
|
456
|
+
/** Update the stored version (called after successful migration). */
|
|
451
457
|
updateVersion(name, version) {
|
|
452
458
|
const entry = this.data.installed[name];
|
|
453
459
|
if (!entry) return;
|
|
454
460
|
entry.version = version;
|
|
455
461
|
entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
456
462
|
}
|
|
463
|
+
/** Return only enabled plugins. */
|
|
457
464
|
listEnabled() {
|
|
458
465
|
return new Map(Object.entries(this.data.installed).filter(([, e]) => e.enabled));
|
|
459
466
|
}
|
|
467
|
+
/** Filter plugins by installation source. */
|
|
460
468
|
listBySource(source) {
|
|
461
469
|
return new Map(Object.entries(this.data.installed).filter(([, e]) => e.source === source));
|
|
462
470
|
}
|
|
471
|
+
/** Load registry data from disk. Silently starts empty if file doesn't exist. */
|
|
463
472
|
async load() {
|
|
464
473
|
try {
|
|
465
474
|
const content = fs4.readFileSync(this.registryPath, "utf-8");
|
|
@@ -471,6 +480,7 @@ var init_plugin_registry = __esm({
|
|
|
471
480
|
this.data = { installed: {} };
|
|
472
481
|
}
|
|
473
482
|
}
|
|
483
|
+
/** Persist registry data to disk. */
|
|
474
484
|
async save() {
|
|
475
485
|
const dir = path4.dirname(this.registryPath);
|
|
476
486
|
fs4.mkdirSync(dir, { recursive: true });
|
|
@@ -497,6 +507,7 @@ var init_registry_client = __esm({
|
|
|
497
507
|
constructor(registryUrl) {
|
|
498
508
|
this.registryUrl = registryUrl ?? REGISTRY_URL;
|
|
499
509
|
}
|
|
510
|
+
/** Fetch the registry, returning cached data if still fresh. */
|
|
500
511
|
async getRegistry() {
|
|
501
512
|
if (this.cache && Date.now() - this.cache.fetchedAt < CACHE_TTL) {
|
|
502
513
|
return this.cache.data;
|
|
@@ -507,6 +518,7 @@ var init_registry_client = __esm({
|
|
|
507
518
|
this.cache = { data, fetchedAt: Date.now() };
|
|
508
519
|
return data;
|
|
509
520
|
}
|
|
521
|
+
/** Search plugins by name, description, or tags (case-insensitive substring match). */
|
|
510
522
|
async search(query) {
|
|
511
523
|
const registry = await this.getRegistry();
|
|
512
524
|
const q = query.toLowerCase();
|
|
@@ -515,11 +527,13 @@ var init_registry_client = __esm({
|
|
|
515
527
|
return text5.includes(q);
|
|
516
528
|
});
|
|
517
529
|
}
|
|
530
|
+
/** Resolve a registry plugin name to its npm package name. Returns null if not found. */
|
|
518
531
|
async resolve(name) {
|
|
519
532
|
const registry = await this.getRegistry();
|
|
520
533
|
const plugin2 = registry.plugins.find((p2) => p2.name === name);
|
|
521
534
|
return plugin2?.npm ?? null;
|
|
522
535
|
}
|
|
536
|
+
/** Force next getRegistry() call to refetch from network. */
|
|
523
537
|
clearCache() {
|
|
524
538
|
this.cache = null;
|
|
525
539
|
}
|
|
@@ -1738,6 +1752,16 @@ var init_security_guard = __esm({
|
|
|
1738
1752
|
this.getSecurityConfig = getSecurityConfig;
|
|
1739
1753
|
this.sessionManager = sessionManager;
|
|
1740
1754
|
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Returns `{ allowed: true }` when the message may proceed, or
|
|
1757
|
+
* `{ allowed: false, reason }` when it should be blocked.
|
|
1758
|
+
*
|
|
1759
|
+
* Two checks run in order:
|
|
1760
|
+
* 1. **Allowlist** — if `allowedUserIds` is non-empty, the user's ID (coerced to string)
|
|
1761
|
+
* must appear in the list. Telegram/Slack IDs are numbers, so coercion is required.
|
|
1762
|
+
* 2. **Session cap** — counts sessions in `active` or `initializing` state. `initializing`
|
|
1763
|
+
* is included because a session holds resources before it reaches `active`.
|
|
1764
|
+
*/
|
|
1741
1765
|
async checkAccess(message) {
|
|
1742
1766
|
const config = await this.getSecurityConfig();
|
|
1743
1767
|
const allowedIds = config.allowedUserIds ?? [];
|
|
@@ -1813,44 +1837,75 @@ var init_events = __esm({
|
|
|
1813
1837
|
};
|
|
1814
1838
|
BusEvent = {
|
|
1815
1839
|
// --- Session lifecycle ---
|
|
1840
|
+
/** Fired when a new session is created and ready. */
|
|
1816
1841
|
SESSION_CREATED: "session:created",
|
|
1842
|
+
/** Fired when session metadata changes (status, name, overrides). */
|
|
1817
1843
|
SESSION_UPDATED: "session:updated",
|
|
1844
|
+
/** Fired when a session record is deleted from the store. */
|
|
1818
1845
|
SESSION_DELETED: "session:deleted",
|
|
1846
|
+
/** Fired when a session ends (agent finished or error). */
|
|
1819
1847
|
SESSION_ENDED: "session:ended",
|
|
1848
|
+
/** Fired when a session receives its auto-generated name. */
|
|
1820
1849
|
SESSION_NAMED: "session:named",
|
|
1850
|
+
/** Fired after a new session thread is created and bridge connected. */
|
|
1821
1851
|
SESSION_THREAD_READY: "session:threadReady",
|
|
1852
|
+
/** Fired when an agent's config options change (adapters update control UIs). */
|
|
1822
1853
|
SESSION_CONFIG_CHANGED: "session:configChanged",
|
|
1854
|
+
/** Fired during agent switch lifecycle (starting/succeeded/failed). */
|
|
1823
1855
|
SESSION_AGENT_SWITCH: "session:agentSwitch",
|
|
1824
1856
|
// --- Agent ---
|
|
1857
|
+
/** Fired for every agent event (text, tool_call, usage, etc.). */
|
|
1825
1858
|
AGENT_EVENT: "agent:event",
|
|
1859
|
+
/** Fired when a prompt is sent to the agent. */
|
|
1826
1860
|
AGENT_PROMPT: "agent:prompt",
|
|
1827
1861
|
// --- Permissions ---
|
|
1862
|
+
/** Fired when the agent requests user permission (blocks until resolved). */
|
|
1828
1863
|
PERMISSION_REQUEST: "permission:request",
|
|
1864
|
+
/** Fired after a permission request is resolved (approved or denied). */
|
|
1829
1865
|
PERMISSION_RESOLVED: "permission:resolved",
|
|
1830
1866
|
// --- Message visibility ---
|
|
1867
|
+
/** Fired when a user message is queued (for cross-adapter input visibility). */
|
|
1831
1868
|
MESSAGE_QUEUED: "message:queued",
|
|
1869
|
+
/** Fired when a queued message starts processing. */
|
|
1832
1870
|
MESSAGE_PROCESSING: "message:processing",
|
|
1833
1871
|
// --- System lifecycle ---
|
|
1872
|
+
/** Fired after kernel (core + plugin infrastructure) has booted. */
|
|
1834
1873
|
KERNEL_BOOTED: "kernel:booted",
|
|
1874
|
+
/** Fired when the system is fully ready (all adapters connected). */
|
|
1835
1875
|
SYSTEM_READY: "system:ready",
|
|
1876
|
+
/** Fired during graceful shutdown. */
|
|
1836
1877
|
SYSTEM_SHUTDOWN: "system:shutdown",
|
|
1878
|
+
/** Fired when all system commands are registered and available. */
|
|
1837
1879
|
SYSTEM_COMMANDS_READY: "system:commands-ready",
|
|
1838
1880
|
// --- Plugin lifecycle ---
|
|
1881
|
+
/** Fired when a plugin loads successfully. */
|
|
1839
1882
|
PLUGIN_LOADED: "plugin:loaded",
|
|
1883
|
+
/** Fired when a plugin fails to load. */
|
|
1840
1884
|
PLUGIN_FAILED: "plugin:failed",
|
|
1885
|
+
/** Fired when a plugin is disabled (e.g., missing config). */
|
|
1841
1886
|
PLUGIN_DISABLED: "plugin:disabled",
|
|
1887
|
+
/** Fired when a plugin is unloaded during shutdown. */
|
|
1842
1888
|
PLUGIN_UNLOADED: "plugin:unloaded",
|
|
1843
1889
|
// --- Usage ---
|
|
1890
|
+
/** Fired when a token usage record is captured (consumed by usage plugin). */
|
|
1844
1891
|
USAGE_RECORDED: "usage:recorded"
|
|
1845
1892
|
};
|
|
1846
1893
|
SessionEv = {
|
|
1894
|
+
/** Agent produced an event (text, tool_call, etc.) during a turn. */
|
|
1847
1895
|
AGENT_EVENT: "agent_event",
|
|
1896
|
+
/** Agent is requesting user permission — blocks until resolved. */
|
|
1848
1897
|
PERMISSION_REQUEST: "permission_request",
|
|
1898
|
+
/** Session ended (agent finished, cancelled, or errored). */
|
|
1849
1899
|
SESSION_END: "session_end",
|
|
1900
|
+
/** Session status changed (e.g., initializing → active). */
|
|
1850
1901
|
STATUS_CHANGE: "status_change",
|
|
1902
|
+
/** Session received an auto-generated name from the first response. */
|
|
1851
1903
|
NAMED: "named",
|
|
1904
|
+
/** An unrecoverable error occurred in the session. */
|
|
1852
1905
|
ERROR: "error",
|
|
1906
|
+
/** The session's prompt count changed (used for UI counters). */
|
|
1853
1907
|
PROMPT_COUNT_CHANGED: "prompt_count_changed",
|
|
1908
|
+
/** A new prompt turn started (provides TurnContext for middleware). */
|
|
1854
1909
|
TURN_STARTED: "turn_started"
|
|
1855
1910
|
};
|
|
1856
1911
|
}
|
|
@@ -1868,6 +1923,7 @@ function createSecurityPlugin() {
|
|
|
1868
1923
|
description: "User access control and session limits",
|
|
1869
1924
|
essential: false,
|
|
1870
1925
|
permissions: ["services:register", "middleware:register", "kernel:access", "commands:register"],
|
|
1926
|
+
// These keys are propagated to child plugin configs so adapters can read them directly.
|
|
1871
1927
|
inheritableKeys: ["allowedUserIds", "maxConcurrentSessions", "sessionTimeoutMinutes"],
|
|
1872
1928
|
async install(ctx) {
|
|
1873
1929
|
await ctx.settings.setAll({
|
|
@@ -2074,6 +2130,14 @@ var init_file_service = __esm({
|
|
|
2074
2130
|
}
|
|
2075
2131
|
return removed;
|
|
2076
2132
|
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Persist a file to the session's directory and return an `Attachment` descriptor.
|
|
2135
|
+
*
|
|
2136
|
+
* The file name is sanitized (non-safe characters replaced with `_`) and prefixed
|
|
2137
|
+
* with a millisecond timestamp to prevent name collisions across multiple saves in
|
|
2138
|
+
* the same session. The original `fileName` is preserved in the returned descriptor
|
|
2139
|
+
* so the user-facing name is not lost.
|
|
2140
|
+
*/
|
|
2077
2141
|
async saveFile(sessionId, fileName, data, mimeType) {
|
|
2078
2142
|
const sessionDir = path6.join(this.baseDir, sessionId);
|
|
2079
2143
|
await fs7.promises.mkdir(sessionDir, { recursive: true });
|
|
@@ -2088,6 +2152,10 @@ var init_file_service = __esm({
|
|
|
2088
2152
|
size: data.length
|
|
2089
2153
|
};
|
|
2090
2154
|
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Build an `Attachment` descriptor for a file that already exists on disk.
|
|
2157
|
+
* Returns `null` if the path does not exist or is not a regular file.
|
|
2158
|
+
*/
|
|
2091
2159
|
async resolveFile(filePath) {
|
|
2092
2160
|
try {
|
|
2093
2161
|
const stat = await fs7.promises.stat(filePath);
|
|
@@ -2132,6 +2200,7 @@ var init_file_service = __esm({
|
|
|
2132
2200
|
extensionFromMime(mimeType) {
|
|
2133
2201
|
return _FileService.extensionFromMime(mimeType);
|
|
2134
2202
|
}
|
|
2203
|
+
/** Returns the canonical file extension for a given MIME type (e.g. `"image/png"` → `".png"`). */
|
|
2135
2204
|
static extensionFromMime(mimeType) {
|
|
2136
2205
|
return MIME_TO_EXT[mimeType] || ".bin";
|
|
2137
2206
|
}
|
|
@@ -2258,6 +2327,12 @@ var init_context_manager = __esm({
|
|
|
2258
2327
|
constructor(cachePath) {
|
|
2259
2328
|
this.cache = new ContextCache(cachePath);
|
|
2260
2329
|
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Wire in the history store after construction.
|
|
2332
|
+
*
|
|
2333
|
+
* Injected separately because the history store (backed by the context plugin's
|
|
2334
|
+
* recorder) may not be ready when ContextManager is first instantiated.
|
|
2335
|
+
*/
|
|
2261
2336
|
setHistoryStore(store) {
|
|
2262
2337
|
this.historyStore = store;
|
|
2263
2338
|
}
|
|
@@ -2273,19 +2348,43 @@ var init_context_manager = __esm({
|
|
|
2273
2348
|
async flushSession(sessionId) {
|
|
2274
2349
|
if (this.sessionFlusher) await this.sessionFlusher(sessionId);
|
|
2275
2350
|
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Read the raw history for a session directly from the history store.
|
|
2353
|
+
*
|
|
2354
|
+
* Returns null if no historyStore has been configured via `setHistoryStore()`,
|
|
2355
|
+
* or if the session has no recorded history.
|
|
2356
|
+
*/
|
|
2276
2357
|
async getHistory(sessionId) {
|
|
2277
2358
|
if (!this.historyStore) return null;
|
|
2278
2359
|
return this.historyStore.read(sessionId);
|
|
2279
2360
|
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Register a provider. Providers are queried in insertion order.
|
|
2363
|
+
* Register higher-priority sources (e.g. local history) before lower-priority ones (e.g. entire).
|
|
2364
|
+
*/
|
|
2280
2365
|
register(provider) {
|
|
2281
2366
|
this.providers.push(provider);
|
|
2282
2367
|
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Return the first provider that reports itself available for the given repo.
|
|
2370
|
+
*
|
|
2371
|
+
* This is a availability check — it returns the highest-priority available provider
|
|
2372
|
+
* (i.e. the first registered one that passes `isAvailable`), not necessarily the
|
|
2373
|
+
* one that would yield the richest context for a specific query.
|
|
2374
|
+
*/
|
|
2283
2375
|
async getProvider(repoPath) {
|
|
2284
2376
|
for (const provider of this.providers) {
|
|
2285
2377
|
if (await provider.isAvailable(repoPath)) return provider;
|
|
2286
2378
|
}
|
|
2287
2379
|
return null;
|
|
2288
2380
|
}
|
|
2381
|
+
/**
|
|
2382
|
+
* List sessions using the same provider-waterfall logic as `buildContext`.
|
|
2383
|
+
*
|
|
2384
|
+
* Tries each registered provider in order, returning the first non-empty result.
|
|
2385
|
+
* Unlike `buildContext`, results are not cached — callers should avoid calling
|
|
2386
|
+
* this in hot paths.
|
|
2387
|
+
*/
|
|
2289
2388
|
async listSessions(query) {
|
|
2290
2389
|
for (const provider of this.providers) {
|
|
2291
2390
|
if (!await provider.isAvailable(query.repoPath)) continue;
|
|
@@ -2294,6 +2393,13 @@ var init_context_manager = __esm({
|
|
|
2294
2393
|
}
|
|
2295
2394
|
return null;
|
|
2296
2395
|
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Build a context block for injection into an agent prompt.
|
|
2398
|
+
*
|
|
2399
|
+
* Tries each registered provider in order. Results are cached by (repoPath + queryKey)
|
|
2400
|
+
* to avoid redundant disk reads. Pass `options.noCache = true` when the caller knows
|
|
2401
|
+
* the history just changed (e.g. immediately after an agent switch + flush).
|
|
2402
|
+
*/
|
|
2297
2403
|
async buildContext(query, options) {
|
|
2298
2404
|
const queryKey = `${query.type}:${query.value}:${options?.limit ?? ""}:${options?.maxTokens ?? ""}:${options?.labelAgent ?? ""}`;
|
|
2299
2405
|
if (!options?.noCache) {
|
|
@@ -2456,8 +2562,9 @@ var init_checkpoint_reader = __esm({
|
|
|
2456
2562
|
sessionIndex: String(idx),
|
|
2457
2563
|
transcriptPath,
|
|
2458
2564
|
createdAt,
|
|
2565
|
+
// endedAt isn't stored in the checkpoint metadata; EntireProvider fills
|
|
2566
|
+
// it from the last turn timestamp when parsing the JSONL transcript.
|
|
2459
2567
|
endedAt: createdAt,
|
|
2460
|
-
// will be filled from JSONL by conversation builder
|
|
2461
2568
|
branch: smeta.branch ?? cpMeta.branch ?? "",
|
|
2462
2569
|
agent: smeta.agent ?? "",
|
|
2463
2570
|
turnCount: smeta.session_metrics?.turn_count ?? 0,
|
|
@@ -3588,6 +3695,7 @@ var init_history_recorder = __esm({
|
|
|
3588
3695
|
status: event.status
|
|
3589
3696
|
};
|
|
3590
3697
|
if (event.kind) step2.kind = event.kind;
|
|
3698
|
+
if (event.rawInput !== void 0) step2.input = event.rawInput;
|
|
3591
3699
|
steps.push(step2);
|
|
3592
3700
|
break;
|
|
3593
3701
|
}
|
|
@@ -3744,6 +3852,8 @@ var init_history_recorder = __esm({
|
|
|
3744
3852
|
this.debounceTimers.delete(sessionId);
|
|
3745
3853
|
}
|
|
3746
3854
|
}
|
|
3855
|
+
// Search backwards so we match the most recent tool call with this ID first,
|
|
3856
|
+
// in case the same tool is invoked multiple times within one turn.
|
|
3747
3857
|
findToolCall(steps, id) {
|
|
3748
3858
|
for (let i = steps.length - 1; i >= 0; i--) {
|
|
3749
3859
|
const s = steps[i];
|
|
@@ -3931,23 +4041,38 @@ var init_speech_service = __esm({
|
|
|
3931
4041
|
setProviderFactory(factory) {
|
|
3932
4042
|
this.providerFactory = factory;
|
|
3933
4043
|
}
|
|
4044
|
+
/** Register an STT provider by name. Overwrites any existing provider with the same name. */
|
|
3934
4045
|
registerSTTProvider(name, provider) {
|
|
3935
4046
|
this.sttProviders.set(name, provider);
|
|
3936
4047
|
}
|
|
4048
|
+
/** Register a TTS provider by name. Called by external TTS plugins (e.g. msedge-tts-plugin). */
|
|
3937
4049
|
registerTTSProvider(name, provider) {
|
|
3938
4050
|
this.ttsProviders.set(name, provider);
|
|
3939
4051
|
}
|
|
4052
|
+
/** Remove a TTS provider — called by external plugins on teardown. */
|
|
3940
4053
|
unregisterTTSProvider(name) {
|
|
3941
4054
|
this.ttsProviders.delete(name);
|
|
3942
4055
|
}
|
|
4056
|
+
/** Returns true if an STT provider is configured and has credentials. */
|
|
3943
4057
|
isSTTAvailable() {
|
|
3944
4058
|
const { provider, providers } = this.config.stt;
|
|
3945
4059
|
return provider !== null && providers[provider]?.apiKey !== void 0;
|
|
3946
4060
|
}
|
|
4061
|
+
/**
|
|
4062
|
+
* Returns true if a TTS provider is configured and an implementation is registered.
|
|
4063
|
+
*
|
|
4064
|
+
* Config alone is not enough — the TTS provider plugin must have registered
|
|
4065
|
+
* its implementation via `registerTTSProvider` before this returns true.
|
|
4066
|
+
*/
|
|
3947
4067
|
isTTSAvailable() {
|
|
3948
4068
|
const provider = this.config.tts.provider;
|
|
3949
4069
|
return provider !== null && this.ttsProviders.has(provider);
|
|
3950
4070
|
}
|
|
4071
|
+
/**
|
|
4072
|
+
* Transcribes audio using the configured STT provider.
|
|
4073
|
+
*
|
|
4074
|
+
* @throws if no STT provider is configured or if the named provider is not registered.
|
|
4075
|
+
*/
|
|
3951
4076
|
async transcribe(audioBuffer, mimeType, options) {
|
|
3952
4077
|
const providerName = this.config.stt.provider;
|
|
3953
4078
|
if (!providerName || !this.config.stt.providers[providerName]?.apiKey) {
|
|
@@ -3959,6 +4084,11 @@ var init_speech_service = __esm({
|
|
|
3959
4084
|
}
|
|
3960
4085
|
return provider.transcribe(audioBuffer, mimeType, options);
|
|
3961
4086
|
}
|
|
4087
|
+
/**
|
|
4088
|
+
* Synthesizes speech using the configured TTS provider.
|
|
4089
|
+
*
|
|
4090
|
+
* @throws if no TTS provider is configured or if the named provider is not registered.
|
|
4091
|
+
*/
|
|
3962
4092
|
async synthesize(text5, options) {
|
|
3963
4093
|
const providerName = this.config.tts.provider;
|
|
3964
4094
|
if (!providerName) {
|
|
@@ -3970,10 +4100,17 @@ var init_speech_service = __esm({
|
|
|
3970
4100
|
}
|
|
3971
4101
|
return provider.synthesize(text5, options);
|
|
3972
4102
|
}
|
|
4103
|
+
/** Replace the active config without rebuilding providers. Use `refreshProviders` to also rebuild. */
|
|
3973
4104
|
updateConfig(config) {
|
|
3974
4105
|
this.config = config;
|
|
3975
4106
|
}
|
|
3976
|
-
/**
|
|
4107
|
+
/**
|
|
4108
|
+
* Reloads TTS and STT providers from a new config snapshot.
|
|
4109
|
+
*
|
|
4110
|
+
* Called after config changes or plugin hot-reload. Factory-managed providers are
|
|
4111
|
+
* rebuilt via the registered `ProviderFactory`; externally-registered providers
|
|
4112
|
+
* (e.g. from `@openacp/msedge-tts-plugin`) are preserved rather than discarded.
|
|
4113
|
+
*/
|
|
3977
4114
|
refreshProviders(newConfig) {
|
|
3978
4115
|
this.config = newConfig;
|
|
3979
4116
|
if (this.providerFactory) {
|
|
@@ -4013,6 +4150,12 @@ var init_groq = __esm({
|
|
|
4013
4150
|
this.defaultModel = defaultModel;
|
|
4014
4151
|
}
|
|
4015
4152
|
name = "groq";
|
|
4153
|
+
/**
|
|
4154
|
+
* Transcribes audio using the Groq Whisper API.
|
|
4155
|
+
*
|
|
4156
|
+
* `verbose_json` response format is requested so the API returns language
|
|
4157
|
+
* detection and duration metadata alongside the transcript text.
|
|
4158
|
+
*/
|
|
4016
4159
|
async transcribe(audioBuffer, mimeType, options) {
|
|
4017
4160
|
const ext = mimeToExt(mimeType);
|
|
4018
4161
|
const form = new FormData();
|
|
@@ -4138,6 +4281,7 @@ var init_speech = __esm({
|
|
|
4138
4281
|
version: "1.0.0",
|
|
4139
4282
|
description: "Text-to-speech and speech-to-text with pluggable providers",
|
|
4140
4283
|
essential: false,
|
|
4284
|
+
// file-service is needed to persist synthesized audio for adapters that send files
|
|
4141
4285
|
optionalPluginDependencies: { "@openacp/file-service": "^1.0.0" },
|
|
4142
4286
|
permissions: ["services:register", "commands:register", "kernel:access"],
|
|
4143
4287
|
inheritableKeys: ["ttsProvider", "ttsVoice"],
|
|
@@ -4349,6 +4493,12 @@ var init_notification = __esm({
|
|
|
4349
4493
|
constructor(adapters) {
|
|
4350
4494
|
this.adapters = adapters;
|
|
4351
4495
|
}
|
|
4496
|
+
/**
|
|
4497
|
+
* Send a notification to a specific channel adapter.
|
|
4498
|
+
*
|
|
4499
|
+
* Failures are swallowed — notifications are best-effort and must not crash
|
|
4500
|
+
* the session or caller (e.g. on session completion).
|
|
4501
|
+
*/
|
|
4352
4502
|
async notify(channelId, notification) {
|
|
4353
4503
|
const adapter = this.adapters.get(channelId);
|
|
4354
4504
|
if (!adapter) return;
|
|
@@ -4357,6 +4507,12 @@ var init_notification = __esm({
|
|
|
4357
4507
|
} catch {
|
|
4358
4508
|
}
|
|
4359
4509
|
}
|
|
4510
|
+
/**
|
|
4511
|
+
* Broadcast a notification to every registered adapter.
|
|
4512
|
+
*
|
|
4513
|
+
* Used for system-wide alerts (e.g. global budget exhausted). Each adapter
|
|
4514
|
+
* failure is isolated so one broken adapter cannot block the rest.
|
|
4515
|
+
*/
|
|
4360
4516
|
async notifyAll(notification) {
|
|
4361
4517
|
for (const adapter of this.adapters.values()) {
|
|
4362
4518
|
try {
|
|
@@ -4380,6 +4536,7 @@ function createNotificationsPlugin() {
|
|
|
4380
4536
|
version: "1.0.0",
|
|
4381
4537
|
description: "Cross-session notification routing",
|
|
4382
4538
|
essential: false,
|
|
4539
|
+
// Depends on security so the notification service is only active for authorized sessions
|
|
4383
4540
|
pluginDependencies: { "@openacp/security": "^1.0.0" },
|
|
4384
4541
|
permissions: ["services:register", "kernel:access"],
|
|
4385
4542
|
async install(ctx) {
|
|
@@ -4434,6 +4591,10 @@ var init_keepalive = __esm({
|
|
|
4434
4591
|
static PING_INTERVAL = 3e4;
|
|
4435
4592
|
static FAIL_THRESHOLD = 3;
|
|
4436
4593
|
static PING_TIMEOUT = 5e3;
|
|
4594
|
+
/**
|
|
4595
|
+
* Start polling. Replaces any existing interval.
|
|
4596
|
+
* `onDead` is called once when the failure threshold is reached.
|
|
4597
|
+
*/
|
|
4437
4598
|
start(tunnelUrl, onDead) {
|
|
4438
4599
|
this.stop();
|
|
4439
4600
|
this.interval = setInterval(async () => {
|
|
@@ -4455,6 +4616,10 @@ var init_keepalive = __esm({
|
|
|
4455
4616
|
}
|
|
4456
4617
|
}, _TunnelKeepAlive.PING_INTERVAL);
|
|
4457
4618
|
}
|
|
4619
|
+
/**
|
|
4620
|
+
* Stop the keepalive interval and reset the failure counter.
|
|
4621
|
+
* Resetting ensures a clean slate if the keepalive is restarted after stopping.
|
|
4622
|
+
*/
|
|
4458
4623
|
stop() {
|
|
4459
4624
|
if (this.interval) {
|
|
4460
4625
|
clearInterval(this.interval);
|
|
@@ -5741,6 +5906,8 @@ var init_openacp = __esm({
|
|
|
5741
5906
|
getPublicUrl() {
|
|
5742
5907
|
return this.publicUrl;
|
|
5743
5908
|
}
|
|
5909
|
+
// Try to reuse a persisted tunnel (stable URL across restarts).
|
|
5910
|
+
// If the saved tunnel is no longer alive on the worker, create a fresh one.
|
|
5744
5911
|
async resolveCredentials(saved, all, localPort) {
|
|
5745
5912
|
if (saved) {
|
|
5746
5913
|
const alive = await this.pingWorker(saved.tunnelId);
|
|
@@ -5904,6 +6071,13 @@ var init_tunnel_registry = __esm({
|
|
|
5904
6071
|
this.binDir = opts.binDir;
|
|
5905
6072
|
this.storage = opts.storage ?? null;
|
|
5906
6073
|
}
|
|
6074
|
+
/**
|
|
6075
|
+
* Spawn a new tunnel process for the given port and register it.
|
|
6076
|
+
*
|
|
6077
|
+
* Persists the entry to `tunnels.json` once the tunnel reaches `active` status.
|
|
6078
|
+
* Throws if the port is already in use by an active or starting tunnel, or if the
|
|
6079
|
+
* user tunnel limit is reached.
|
|
6080
|
+
*/
|
|
5907
6081
|
async add(port, opts, _autoRetry = true) {
|
|
5908
6082
|
if (this.entries.has(port)) {
|
|
5909
6083
|
const existing = this.entries.get(port);
|
|
@@ -6022,6 +6196,12 @@ var init_tunnel_registry = __esm({
|
|
|
6022
6196
|
this.scheduleSave();
|
|
6023
6197
|
}
|
|
6024
6198
|
}
|
|
6199
|
+
/**
|
|
6200
|
+
* Stop a user tunnel by port and remove it from the registry.
|
|
6201
|
+
*
|
|
6202
|
+
* Cancels any pending retry timer and waits for an in-progress spawn to settle
|
|
6203
|
+
* before terminating the process. Throws if the port is a system tunnel.
|
|
6204
|
+
*/
|
|
6025
6205
|
async stop(port) {
|
|
6026
6206
|
const live = this.entries.get(port);
|
|
6027
6207
|
if (!live) return;
|
|
@@ -6054,6 +6234,7 @@ var init_tunnel_registry = __esm({
|
|
|
6054
6234
|
}
|
|
6055
6235
|
return stopped;
|
|
6056
6236
|
}
|
|
6237
|
+
/** Stop all user tunnels. Errors from individual stops are silently ignored. */
|
|
6057
6238
|
async stopAllUser() {
|
|
6058
6239
|
const userEntries = this.list(false);
|
|
6059
6240
|
for (const entry of userEntries) {
|
|
@@ -6083,6 +6264,12 @@ var init_tunnel_registry = __esm({
|
|
|
6083
6264
|
this.save();
|
|
6084
6265
|
this.entries.clear();
|
|
6085
6266
|
}
|
|
6267
|
+
/**
|
|
6268
|
+
* Return all current tunnel entries.
|
|
6269
|
+
*
|
|
6270
|
+
* Pass `includeSystem = true` to include the system tunnel in the result;
|
|
6271
|
+
* by default only user tunnels are returned.
|
|
6272
|
+
*/
|
|
6086
6273
|
list(includeSystem = false) {
|
|
6087
6274
|
const entries = Array.from(this.entries.values()).map((l) => l.entry);
|
|
6088
6275
|
if (includeSystem) return entries;
|
|
@@ -6094,12 +6281,23 @@ var init_tunnel_registry = __esm({
|
|
|
6094
6281
|
getBySession(sessionId) {
|
|
6095
6282
|
return this.list(false).filter((e) => e.sessionId === sessionId);
|
|
6096
6283
|
}
|
|
6284
|
+
/**
|
|
6285
|
+
* Return the system tunnel entry, or null if it hasn't been registered yet
|
|
6286
|
+
* (e.g. `TunnelService.start()` hasn't been called, or the tunnel failed to start).
|
|
6287
|
+
*/
|
|
6097
6288
|
getSystemEntry() {
|
|
6098
6289
|
for (const live of this.entries.values()) {
|
|
6099
6290
|
if (live.entry.type === "system") return live.entry;
|
|
6100
6291
|
}
|
|
6101
6292
|
return null;
|
|
6102
6293
|
}
|
|
6294
|
+
/**
|
|
6295
|
+
* Re-launch tunnels persisted from a previous run.
|
|
6296
|
+
*
|
|
6297
|
+
* Only user tunnels are restored — the system tunnel is registered separately by
|
|
6298
|
+
* `TunnelService.start()`. `sessionId` is intentionally dropped: sessions do not
|
|
6299
|
+
* survive a restart, so restored tunnels are session-less until an agent claims them.
|
|
6300
|
+
*/
|
|
6103
6301
|
async restore() {
|
|
6104
6302
|
if (!fs16.existsSync(this.registryPath)) return;
|
|
6105
6303
|
try {
|
|
@@ -6112,7 +6310,6 @@ var init_tunnel_registry = __esm({
|
|
|
6112
6310
|
type: persisted.type,
|
|
6113
6311
|
provider: persisted.provider,
|
|
6114
6312
|
label: persisted.label
|
|
6115
|
-
// sessionId intentionally omitted — sessions don't survive restart
|
|
6116
6313
|
})
|
|
6117
6314
|
)
|
|
6118
6315
|
);
|
|
@@ -6151,6 +6348,8 @@ var init_tunnel_registry = __esm({
|
|
|
6151
6348
|
return new OpenACPTunnelProvider(this.providerOptions, this.binDir ?? "", this.storage);
|
|
6152
6349
|
}
|
|
6153
6350
|
}
|
|
6351
|
+
// Debounce disk writes — multiple tunnel state changes may occur in rapid succession
|
|
6352
|
+
// (e.g. status update + URL assignment). Coalesce them into a single write after 2s.
|
|
6154
6353
|
scheduleSave() {
|
|
6155
6354
|
if (this.saveTimeout) clearTimeout(this.saveTimeout);
|
|
6156
6355
|
this.saveTimeout = setTimeout(() => this.save(), 2e3);
|
|
@@ -6814,6 +7013,9 @@ var init_viewer_store = __esm({
|
|
|
6814
7013
|
log10.debug({ removed, remaining: this.entries.size }, "Cleaned up expired viewer entries");
|
|
6815
7014
|
}
|
|
6816
7015
|
}
|
|
7016
|
+
// Guard against agents trying to serve files outside the session workspace
|
|
7017
|
+
// (e.g. /etc/passwd or ~/ paths). Uses realpath to handle symlinks and canonicalize
|
|
7018
|
+
// case on macOS/Windows where the filesystem is case-insensitive.
|
|
6817
7019
|
isPathAllowed(filePath, workingDirectory) {
|
|
6818
7020
|
const caseInsensitive = process.platform === "darwin" || process.platform === "win32";
|
|
6819
7021
|
let resolved;
|
|
@@ -7254,6 +7456,7 @@ var init_token_store = __esm({
|
|
|
7254
7456
|
codes = /* @__PURE__ */ new Map();
|
|
7255
7457
|
savePromise = null;
|
|
7256
7458
|
savePending = false;
|
|
7459
|
+
/** Loads token and code state from disk. Safe to call at startup; missing file is not an error. */
|
|
7257
7460
|
async load() {
|
|
7258
7461
|
try {
|
|
7259
7462
|
const data = await readFile2(this.filePath, "utf-8");
|
|
@@ -7284,6 +7487,10 @@ var init_token_store = __esm({
|
|
|
7284
7487
|
};
|
|
7285
7488
|
await writeFile(this.filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
7286
7489
|
}
|
|
7490
|
+
/**
|
|
7491
|
+
* Coalesces concurrent writes: if a save is in-flight, sets a pending flag
|
|
7492
|
+
* so the next save fires immediately after the current one completes.
|
|
7493
|
+
*/
|
|
7287
7494
|
scheduleSave() {
|
|
7288
7495
|
if (this.savePromise) {
|
|
7289
7496
|
this.savePending = true;
|
|
@@ -7299,6 +7506,7 @@ var init_token_store = __esm({
|
|
|
7299
7506
|
}
|
|
7300
7507
|
});
|
|
7301
7508
|
}
|
|
7509
|
+
/** Creates a new token record and schedules a persist. Returns the stored token including its generated id. */
|
|
7302
7510
|
create(opts) {
|
|
7303
7511
|
const now = /* @__PURE__ */ new Date();
|
|
7304
7512
|
const token = {
|
|
@@ -7317,6 +7525,7 @@ var init_token_store = __esm({
|
|
|
7317
7525
|
get(id) {
|
|
7318
7526
|
return this.tokens.get(id);
|
|
7319
7527
|
}
|
|
7528
|
+
/** Marks a token as revoked; future auth checks will reject it immediately. */
|
|
7320
7529
|
revoke(id) {
|
|
7321
7530
|
const token = this.tokens.get(id);
|
|
7322
7531
|
if (token) {
|
|
@@ -7324,10 +7533,17 @@ var init_token_store = __esm({
|
|
|
7324
7533
|
this.scheduleSave();
|
|
7325
7534
|
}
|
|
7326
7535
|
}
|
|
7536
|
+
/** Returns all non-revoked tokens. Revoked tokens are retained until cleanup() removes them. */
|
|
7327
7537
|
list() {
|
|
7328
7538
|
return Array.from(this.tokens.values()).filter((t) => !t.revoked);
|
|
7329
7539
|
}
|
|
7330
7540
|
lastUsedSaveTimer = null;
|
|
7541
|
+
/**
|
|
7542
|
+
* Records the current timestamp as `lastUsedAt` for the given token.
|
|
7543
|
+
*
|
|
7544
|
+
* Writes are debounced to 60 seconds — every API request updates this field,
|
|
7545
|
+
* so flushing on every call would cause excessive disk I/O.
|
|
7546
|
+
*/
|
|
7331
7547
|
updateLastUsed(id) {
|
|
7332
7548
|
const token = this.tokens.get(id);
|
|
7333
7549
|
if (token) {
|
|
@@ -7357,6 +7573,12 @@ var init_token_store = __esm({
|
|
|
7357
7573
|
this.lastUsedSaveTimer = null;
|
|
7358
7574
|
}
|
|
7359
7575
|
}
|
|
7576
|
+
/**
|
|
7577
|
+
* Generates a one-time authorization code that can be exchanged for a JWT.
|
|
7578
|
+
*
|
|
7579
|
+
* Used for the CLI login flow: the server emits a code that the user copies into
|
|
7580
|
+
* the App, which exchanges it for a proper JWT without ever exposing the raw API secret.
|
|
7581
|
+
*/
|
|
7360
7582
|
createCode(opts) {
|
|
7361
7583
|
const code = randomBytes(16).toString("hex");
|
|
7362
7584
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -7382,6 +7604,13 @@ var init_token_store = __esm({
|
|
|
7382
7604
|
if (new Date(stored.expiresAt).getTime() < Date.now()) return void 0;
|
|
7383
7605
|
return stored;
|
|
7384
7606
|
}
|
|
7607
|
+
/**
|
|
7608
|
+
* Atomically marks a code as used and returns it.
|
|
7609
|
+
*
|
|
7610
|
+
* Returns undefined if the code is unknown, already used, or expired.
|
|
7611
|
+
* The one-time-use flag is set before returning, so concurrent calls for the
|
|
7612
|
+
* same code will only succeed once.
|
|
7613
|
+
*/
|
|
7385
7614
|
exchangeCode(code) {
|
|
7386
7615
|
const stored = this.codes.get(code);
|
|
7387
7616
|
if (!stored) return void 0;
|
|
@@ -7401,6 +7630,13 @@ var init_token_store = __esm({
|
|
|
7401
7630
|
this.codes.delete(code);
|
|
7402
7631
|
this.scheduleSave();
|
|
7403
7632
|
}
|
|
7633
|
+
/**
|
|
7634
|
+
* Removes tokens past their refresh deadline and expired/used codes.
|
|
7635
|
+
*
|
|
7636
|
+
* Called on a 1-hour interval from the plugin setup to prevent unbounded file growth.
|
|
7637
|
+
* Tokens within their refresh deadline are retained even if revoked, so that the
|
|
7638
|
+
* "token revoked" error can be returned instead of "token unknown".
|
|
7639
|
+
*/
|
|
7404
7640
|
cleanup() {
|
|
7405
7641
|
const now = Date.now();
|
|
7406
7642
|
for (const [id, token] of this.tokens) {
|
|
@@ -7928,6 +8164,13 @@ var init_sse_manager = __esm({
|
|
|
7928
8164
|
sseCleanupHandlers = /* @__PURE__ */ new Map();
|
|
7929
8165
|
healthInterval;
|
|
7930
8166
|
boundHandlers = [];
|
|
8167
|
+
/**
|
|
8168
|
+
* Subscribes to EventBus events and starts the health heartbeat interval.
|
|
8169
|
+
*
|
|
8170
|
+
* Must be called after the HTTP server is listening, not during plugin setup —
|
|
8171
|
+
* before `setup()` runs, no EventBus listeners are registered, so any events
|
|
8172
|
+
* emitted in the interim are silently missed.
|
|
8173
|
+
*/
|
|
7931
8174
|
setup() {
|
|
7932
8175
|
if (!this.eventBus) return;
|
|
7933
8176
|
const events = [
|
|
@@ -7961,6 +8204,13 @@ var init_sse_manager = __esm({
|
|
|
7961
8204
|
});
|
|
7962
8205
|
}, 15e3);
|
|
7963
8206
|
}
|
|
8207
|
+
/**
|
|
8208
|
+
* Handles an incoming SSE request by upgrading the HTTP response to an event stream.
|
|
8209
|
+
*
|
|
8210
|
+
* The response is kept open indefinitely; cleanup runs when the client disconnects.
|
|
8211
|
+
* An initial `: connected` comment is written immediately so proxies and browsers
|
|
8212
|
+
* flush the response headers before the first real event arrives.
|
|
8213
|
+
*/
|
|
7964
8214
|
handleRequest(req, res) {
|
|
7965
8215
|
if (this.sseConnections.size >= MAX_SSE_CONNECTIONS) {
|
|
7966
8216
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
@@ -7995,6 +8245,13 @@ var init_sse_manager = __esm({
|
|
|
7995
8245
|
this.sseCleanupHandlers.set(res, cleanup);
|
|
7996
8246
|
req.on("close", cleanup);
|
|
7997
8247
|
}
|
|
8248
|
+
/**
|
|
8249
|
+
* Broadcasts an event to all connected SSE clients.
|
|
8250
|
+
*
|
|
8251
|
+
* Session-scoped events (agent_event, permission_request, etc.) are filtered per-connection:
|
|
8252
|
+
* a client that subscribed with `?sessionId=X` only receives events for session X.
|
|
8253
|
+
* Global events (session_created, health) are delivered to every client.
|
|
8254
|
+
*/
|
|
7998
8255
|
broadcast(event, data) {
|
|
7999
8256
|
const payload = `event: ${event}
|
|
8000
8257
|
data: ${JSON.stringify(data)}
|
|
@@ -8030,6 +8287,12 @@ data: ${JSON.stringify(data)}
|
|
|
8030
8287
|
this.handleRequest(request.raw, reply.raw);
|
|
8031
8288
|
};
|
|
8032
8289
|
}
|
|
8290
|
+
/**
|
|
8291
|
+
* Stops the heartbeat, removes all EventBus listeners, and closes all open SSE connections.
|
|
8292
|
+
*
|
|
8293
|
+
* Only listeners registered by this instance are removed — other consumers on the same
|
|
8294
|
+
* EventBus are unaffected.
|
|
8295
|
+
*/
|
|
8033
8296
|
stop() {
|
|
8034
8297
|
if (this.healthInterval) clearInterval(this.healthInterval);
|
|
8035
8298
|
if (this.eventBus) {
|
|
@@ -8093,9 +8356,15 @@ var init_static_server = __esm({
|
|
|
8093
8356
|
}
|
|
8094
8357
|
}
|
|
8095
8358
|
}
|
|
8359
|
+
/** Returns true if a UI build was found and static serving is active. */
|
|
8096
8360
|
isAvailable() {
|
|
8097
8361
|
return this.uiDir !== void 0;
|
|
8098
8362
|
}
|
|
8363
|
+
/**
|
|
8364
|
+
* Attempts to serve a static file or SPA fallback for the given request.
|
|
8365
|
+
*
|
|
8366
|
+
* @returns true if the response was handled, false if the caller should return a 404.
|
|
8367
|
+
*/
|
|
8099
8368
|
serve(req, res) {
|
|
8100
8369
|
if (!this.uiDir) return false;
|
|
8101
8370
|
const urlPath = (req.url || "/").split("?")[0];
|
|
@@ -8828,6 +9097,8 @@ var init_config_migrations = __esm({
|
|
|
8828
9097
|
log13 = createChildLogger({ module: "config-migrations" });
|
|
8829
9098
|
migrations = [
|
|
8830
9099
|
{
|
|
9100
|
+
// v2025.x: instanceName was added to support multi-instance setups.
|
|
9101
|
+
// Old configs lack this field — default to "Main" so the UI has a display name.
|
|
8831
9102
|
name: "add-instance-name",
|
|
8832
9103
|
apply(raw) {
|
|
8833
9104
|
if (raw.instanceName) return false;
|
|
@@ -8837,6 +9108,8 @@ var init_config_migrations = __esm({
|
|
|
8837
9108
|
}
|
|
8838
9109
|
},
|
|
8839
9110
|
{
|
|
9111
|
+
// displayVerbosity was replaced by outputMode — remove the legacy key
|
|
9112
|
+
// so it doesn't confuse Zod strict parsing or the config editor.
|
|
8840
9113
|
name: "delete-display-verbosity",
|
|
8841
9114
|
apply(raw) {
|
|
8842
9115
|
if (!("displayVerbosity" in raw)) return false;
|
|
@@ -8846,6 +9119,9 @@ var init_config_migrations = __esm({
|
|
|
8846
9119
|
}
|
|
8847
9120
|
},
|
|
8848
9121
|
{
|
|
9122
|
+
// Instance IDs were originally only in instances.json (the global registry).
|
|
9123
|
+
// This migration copies the ID into config.json so each instance is self-identifying
|
|
9124
|
+
// without needing to cross-reference the registry.
|
|
8849
9125
|
name: "add-instance-id",
|
|
8850
9126
|
apply(raw, ctx) {
|
|
8851
9127
|
if (raw.id) return false;
|
|
@@ -8906,10 +9182,11 @@ var init_config2 = __esm({
|
|
|
8906
9182
|
sessionLogRetentionDays: z4.number().default(30)
|
|
8907
9183
|
}).default({});
|
|
8908
9184
|
ConfigSchema = z4.object({
|
|
9185
|
+
/** Instance UUID, written once at creation time. */
|
|
8909
9186
|
id: z4.string().optional(),
|
|
8910
|
-
// instance UUID, written once at creation time
|
|
8911
9187
|
instanceName: z4.string().optional(),
|
|
8912
9188
|
defaultAgent: z4.string(),
|
|
9189
|
+
// --- Workspace security & path resolution ---
|
|
8913
9190
|
workspace: z4.object({
|
|
8914
9191
|
allowExternalWorkspaces: z4.boolean().default(true),
|
|
8915
9192
|
security: z4.object({
|
|
@@ -8917,12 +9194,16 @@ var init_config2 = __esm({
|
|
|
8917
9194
|
envWhitelist: z4.array(z4.string()).default([])
|
|
8918
9195
|
}).default({})
|
|
8919
9196
|
}).default({}),
|
|
9197
|
+
// --- Logging ---
|
|
8920
9198
|
logging: LoggingSchema,
|
|
9199
|
+
// --- Process lifecycle ---
|
|
8921
9200
|
runMode: z4.enum(["foreground", "daemon"]).default("foreground"),
|
|
8922
9201
|
autoStart: z4.boolean().default(false),
|
|
9202
|
+
// --- Session persistence ---
|
|
8923
9203
|
sessionStore: z4.object({
|
|
8924
9204
|
ttlDays: z4.number().default(30)
|
|
8925
9205
|
}).default({}),
|
|
9206
|
+
// --- Installed integration tracking (e.g. plugins installed via CLI) ---
|
|
8926
9207
|
integrations: z4.record(
|
|
8927
9208
|
z4.string(),
|
|
8928
9209
|
z4.object({
|
|
@@ -8930,7 +9211,9 @@ var init_config2 = __esm({
|
|
|
8930
9211
|
installedAt: z4.string().optional()
|
|
8931
9212
|
})
|
|
8932
9213
|
).default({}),
|
|
9214
|
+
// --- Agent output verbosity control ---
|
|
8933
9215
|
outputMode: z4.enum(["low", "medium", "high"]).default("medium").optional(),
|
|
9216
|
+
// --- Multi-agent switching behavior ---
|
|
8934
9217
|
agentSwitch: z4.object({
|
|
8935
9218
|
labelHistory: z4.boolean().default(true)
|
|
8936
9219
|
}).default({})
|
|
@@ -8946,6 +9229,13 @@ var init_config2 = __esm({
|
|
|
8946
9229
|
super();
|
|
8947
9230
|
this.configPath = process.env.OPENACP_CONFIG_PATH || configPath || expandHome2("~/.openacp/config.json");
|
|
8948
9231
|
}
|
|
9232
|
+
/**
|
|
9233
|
+
* Loads config from disk through the full validation pipeline:
|
|
9234
|
+
* 1. Create default config if missing (first run)
|
|
9235
|
+
* 2. Apply migrations for older config formats
|
|
9236
|
+
* 3. Apply environment variable overrides
|
|
9237
|
+
* 4. Validate against Zod schema — exits on failure
|
|
9238
|
+
*/
|
|
8949
9239
|
async load() {
|
|
8950
9240
|
const dir = path24.dirname(this.configPath);
|
|
8951
9241
|
fs21.mkdirSync(dir, { recursive: true });
|
|
@@ -8979,9 +9269,17 @@ var init_config2 = __esm({
|
|
|
8979
9269
|
}
|
|
8980
9270
|
this.config = result.data;
|
|
8981
9271
|
}
|
|
9272
|
+
/** Returns a deep clone of the current config to prevent external mutation. */
|
|
8982
9273
|
get() {
|
|
8983
9274
|
return structuredClone(this.config);
|
|
8984
9275
|
}
|
|
9276
|
+
/**
|
|
9277
|
+
* Merges partial updates into the config file using atomic write (write tmp + rename).
|
|
9278
|
+
*
|
|
9279
|
+
* Validates the merged result before writing. If `changePath` is provided,
|
|
9280
|
+
* emits a `config:changed` event with old and new values for that path,
|
|
9281
|
+
* enabling hot-reload without restart.
|
|
9282
|
+
*/
|
|
8985
9283
|
async save(updates, changePath) {
|
|
8986
9284
|
const oldConfig = this.config ? structuredClone(this.config) : void 0;
|
|
8987
9285
|
const raw = JSON.parse(fs21.readFileSync(this.configPath, "utf-8"));
|
|
@@ -9003,9 +9301,12 @@ var init_config2 = __esm({
|
|
|
9003
9301
|
}
|
|
9004
9302
|
}
|
|
9005
9303
|
/**
|
|
9006
|
-
*
|
|
9007
|
-
*
|
|
9008
|
-
*
|
|
9304
|
+
* Convenience wrapper for updating a single deeply-nested config field
|
|
9305
|
+
* without constructing the full update object manually.
|
|
9306
|
+
*
|
|
9307
|
+
* Accepts a dot-path (e.g. "logging.level") and builds the nested
|
|
9308
|
+
* update object internally before delegating to `save()`.
|
|
9309
|
+
* Throws if the path contains prototype-pollution keys.
|
|
9009
9310
|
*/
|
|
9010
9311
|
async setPath(dotPath, value) {
|
|
9011
9312
|
const BLOCKED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
@@ -9022,6 +9323,13 @@ var init_config2 = __esm({
|
|
|
9022
9323
|
target[parts[parts.length - 1]] = value;
|
|
9023
9324
|
await this.save(updates, dotPath);
|
|
9024
9325
|
}
|
|
9326
|
+
/**
|
|
9327
|
+
* Resolves a workspace path from user input.
|
|
9328
|
+
*
|
|
9329
|
+
* Supports three forms: no input (returns base dir), absolute/tilde paths
|
|
9330
|
+
* (validated against allowExternalWorkspaces), and named workspaces
|
|
9331
|
+
* (alphanumeric subdirectories under the base).
|
|
9332
|
+
*/
|
|
9025
9333
|
resolveWorkspace(input2) {
|
|
9026
9334
|
const workspaceBase = path24.dirname(path24.dirname(this.configPath));
|
|
9027
9335
|
if (!input2) {
|
|
@@ -9056,17 +9364,26 @@ var init_config2 = __esm({
|
|
|
9056
9364
|
fs21.mkdirSync(namedPath, { recursive: true });
|
|
9057
9365
|
return namedPath;
|
|
9058
9366
|
}
|
|
9367
|
+
/** Checks whether the config file exists on disk. Wraps synchronous `fs.existsSync` behind an async interface for consistency with the rest of the ConfigManager API. */
|
|
9059
9368
|
async exists() {
|
|
9060
9369
|
return fs21.existsSync(this.configPath);
|
|
9061
9370
|
}
|
|
9371
|
+
/** Returns the resolved path to the config JSON file. */
|
|
9062
9372
|
getConfigPath() {
|
|
9063
9373
|
return this.configPath;
|
|
9064
9374
|
}
|
|
9375
|
+
/** Writes a complete config object to disk, creating the directory if needed. Used during initial setup. */
|
|
9065
9376
|
async writeNew(config) {
|
|
9066
9377
|
const dir = path24.dirname(this.configPath);
|
|
9067
9378
|
fs21.mkdirSync(dir, { recursive: true });
|
|
9068
9379
|
fs21.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
|
|
9069
9380
|
}
|
|
9381
|
+
/**
|
|
9382
|
+
* Applies `OPENACP_*` environment variables as overrides to per-plugin settings.
|
|
9383
|
+
*
|
|
9384
|
+
* This lets users configure plugin values (bot tokens, ports, etc.) via env vars
|
|
9385
|
+
* without editing settings files — useful for Docker, CI, and headless setups.
|
|
9386
|
+
*/
|
|
9070
9387
|
async applyEnvToPluginSettings(settingsManager) {
|
|
9071
9388
|
const pluginOverrides = [
|
|
9072
9389
|
{ envVar: "OPENACP_TUNNEL_ENABLED", pluginName: "@openacp/tunnel", key: "enabled", transform: (v) => v === "true" },
|
|
@@ -9093,6 +9410,7 @@ var init_config2 = __esm({
|
|
|
9093
9410
|
}
|
|
9094
9411
|
}
|
|
9095
9412
|
}
|
|
9413
|
+
/** Applies env var overrides to the raw config object before Zod validation. */
|
|
9096
9414
|
applyEnvOverrides(raw) {
|
|
9097
9415
|
const overrides = [
|
|
9098
9416
|
["OPENACP_DEFAULT_AGENT", ["defaultAgent"]],
|
|
@@ -9123,6 +9441,7 @@ var init_config2 = __esm({
|
|
|
9123
9441
|
raw.logging.level = "debug";
|
|
9124
9442
|
}
|
|
9125
9443
|
}
|
|
9444
|
+
/** Recursively merges source into target, skipping prototype-pollution keys. */
|
|
9126
9445
|
deepMerge(target, source) {
|
|
9127
9446
|
const DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
9128
9447
|
for (const key of Object.keys(source)) {
|
|
@@ -9933,6 +10252,7 @@ var init_instance_registry = __esm({
|
|
|
9933
10252
|
this.registryPath = registryPath;
|
|
9934
10253
|
}
|
|
9935
10254
|
data = { version: 1, instances: {} };
|
|
10255
|
+
/** Load the registry from disk. If the file is missing or corrupt, starts fresh. */
|
|
9936
10256
|
load() {
|
|
9937
10257
|
try {
|
|
9938
10258
|
const raw = fs22.readFileSync(this.registryPath, "utf-8");
|
|
@@ -9960,26 +10280,33 @@ var init_instance_registry = __esm({
|
|
|
9960
10280
|
this.save();
|
|
9961
10281
|
}
|
|
9962
10282
|
}
|
|
10283
|
+
/** Persist the registry to disk, creating parent directories if needed. */
|
|
9963
10284
|
save() {
|
|
9964
10285
|
const dir = path26.dirname(this.registryPath);
|
|
9965
10286
|
fs22.mkdirSync(dir, { recursive: true });
|
|
9966
10287
|
fs22.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
|
|
9967
10288
|
}
|
|
10289
|
+
/** Add or update an instance entry in the registry. Does not persist — call save() after. */
|
|
9968
10290
|
register(id, root) {
|
|
9969
10291
|
this.data.instances[id] = { id, root };
|
|
9970
10292
|
}
|
|
10293
|
+
/** Remove an instance entry. Does not persist — call save() after. */
|
|
9971
10294
|
remove(id) {
|
|
9972
10295
|
delete this.data.instances[id];
|
|
9973
10296
|
}
|
|
10297
|
+
/** Look up an instance by its ID. */
|
|
9974
10298
|
get(id) {
|
|
9975
10299
|
return this.data.instances[id];
|
|
9976
10300
|
}
|
|
10301
|
+
/** Look up an instance by its root directory path. */
|
|
9977
10302
|
getByRoot(root) {
|
|
9978
10303
|
return Object.values(this.data.instances).find((e) => e.root === root);
|
|
9979
10304
|
}
|
|
10305
|
+
/** Returns all registered instances. */
|
|
9980
10306
|
list() {
|
|
9981
10307
|
return Object.values(this.data.instances);
|
|
9982
10308
|
}
|
|
10309
|
+
/** Returns `baseId` if available, otherwise appends `-2`, `-3`, etc. until unique. */
|
|
9983
10310
|
uniqueId(baseId) {
|
|
9984
10311
|
if (!this.data.instances[baseId]) return baseId;
|
|
9985
10312
|
let n = 2;
|
|
@@ -10485,6 +10812,7 @@ var init_connection_manager = __esm({
|
|
|
10485
10812
|
"use strict";
|
|
10486
10813
|
ConnectionManager = class {
|
|
10487
10814
|
connections = /* @__PURE__ */ new Map();
|
|
10815
|
+
// Secondary index: sessionId → Set of connection IDs for O(1) broadcast targeting
|
|
10488
10816
|
sessionIndex = /* @__PURE__ */ new Map();
|
|
10489
10817
|
maxConnectionsPerSession;
|
|
10490
10818
|
maxTotalConnections;
|
|
@@ -10492,6 +10820,14 @@ var init_connection_manager = __esm({
|
|
|
10492
10820
|
this.maxConnectionsPerSession = opts?.maxPerSession ?? 10;
|
|
10493
10821
|
this.maxTotalConnections = opts?.maxTotal ?? 100;
|
|
10494
10822
|
}
|
|
10823
|
+
/**
|
|
10824
|
+
* Registers a new SSE connection for the given session.
|
|
10825
|
+
*
|
|
10826
|
+
* Wires a `close` listener on the response so the connection is automatically
|
|
10827
|
+
* removed when the client disconnects (browser tab closed, network drop, etc.).
|
|
10828
|
+
*
|
|
10829
|
+
* @throws if the global or per-session connection limit is reached.
|
|
10830
|
+
*/
|
|
10495
10831
|
addConnection(sessionId, tokenId, response) {
|
|
10496
10832
|
if (this.connections.size >= this.maxTotalConnections) {
|
|
10497
10833
|
throw new Error("Maximum total connections reached");
|
|
@@ -10512,6 +10848,7 @@ var init_connection_manager = __esm({
|
|
|
10512
10848
|
response.on("close", () => this.removeConnection(id));
|
|
10513
10849
|
return connection;
|
|
10514
10850
|
}
|
|
10851
|
+
/** Remove a connection from both indexes. Called automatically on client disconnect. */
|
|
10515
10852
|
removeConnection(connectionId) {
|
|
10516
10853
|
const conn = this.connections.get(connectionId);
|
|
10517
10854
|
if (!conn) return;
|
|
@@ -10522,11 +10859,20 @@ var init_connection_manager = __esm({
|
|
|
10522
10859
|
if (sessionConns.size === 0) this.sessionIndex.delete(conn.sessionId);
|
|
10523
10860
|
}
|
|
10524
10861
|
}
|
|
10862
|
+
/** Returns all active connections for a session. */
|
|
10525
10863
|
getConnectionsBySession(sessionId) {
|
|
10526
10864
|
const connIds = this.sessionIndex.get(sessionId);
|
|
10527
10865
|
if (!connIds) return [];
|
|
10528
10866
|
return Array.from(connIds).map((id) => this.connections.get(id)).filter((c3) => c3 !== void 0);
|
|
10529
10867
|
}
|
|
10868
|
+
/**
|
|
10869
|
+
* Writes a serialized SSE event to all connections for the given session.
|
|
10870
|
+
*
|
|
10871
|
+
* Backpressure handling: if `response.write()` returns false (OS send buffer full),
|
|
10872
|
+
* the connection is flagged as `backpressured`. On the next write attempt, if it is
|
|
10873
|
+
* still backpressured, the connection is forcibly closed to prevent unbounded memory
|
|
10874
|
+
* growth from queuing writes on a slow or stalled client.
|
|
10875
|
+
*/
|
|
10530
10876
|
broadcast(sessionId, serializedEvent) {
|
|
10531
10877
|
for (const conn of this.getConnectionsBySession(sessionId)) {
|
|
10532
10878
|
if (conn.response.writableEnded) continue;
|
|
@@ -10548,6 +10894,10 @@ var init_connection_manager = __esm({
|
|
|
10548
10894
|
}
|
|
10549
10895
|
}
|
|
10550
10896
|
}
|
|
10897
|
+
/**
|
|
10898
|
+
* Force-close all connections associated with a given auth token.
|
|
10899
|
+
* Called when a token is revoked to immediately terminate those streams.
|
|
10900
|
+
*/
|
|
10551
10901
|
disconnectByToken(tokenId) {
|
|
10552
10902
|
for (const [id, conn] of this.connections) {
|
|
10553
10903
|
if (conn.tokenId === tokenId) {
|
|
@@ -10556,9 +10906,11 @@ var init_connection_manager = __esm({
|
|
|
10556
10906
|
}
|
|
10557
10907
|
}
|
|
10558
10908
|
}
|
|
10909
|
+
/** Returns a snapshot of all active connections (used by the admin endpoint). */
|
|
10559
10910
|
listConnections() {
|
|
10560
10911
|
return Array.from(this.connections.values());
|
|
10561
10912
|
}
|
|
10913
|
+
/** Close all connections and clear all indexes. Called on plugin teardown. */
|
|
10562
10914
|
cleanup() {
|
|
10563
10915
|
for (const [, conn] of this.connections) {
|
|
10564
10916
|
if (!conn.response.writableEnded) conn.response.end();
|
|
@@ -10576,10 +10928,15 @@ var init_event_buffer = __esm({
|
|
|
10576
10928
|
"src/plugins/sse-adapter/event-buffer.ts"() {
|
|
10577
10929
|
"use strict";
|
|
10578
10930
|
EventBuffer = class {
|
|
10931
|
+
/**
|
|
10932
|
+
* @param maxSize Maximum events retained per session. Older events are evicted when
|
|
10933
|
+
* this limit is exceeded. Defaults to 100.
|
|
10934
|
+
*/
|
|
10579
10935
|
constructor(maxSize = 100) {
|
|
10580
10936
|
this.maxSize = maxSize;
|
|
10581
10937
|
}
|
|
10582
10938
|
buffers = /* @__PURE__ */ new Map();
|
|
10939
|
+
/** Append an event to the session's buffer, evicting the oldest entry if at capacity. */
|
|
10583
10940
|
push(sessionId, event) {
|
|
10584
10941
|
let buffer = this.buffers.get(sessionId);
|
|
10585
10942
|
if (!buffer) {
|
|
@@ -10591,6 +10948,14 @@ var init_event_buffer = __esm({
|
|
|
10591
10948
|
buffer.shift();
|
|
10592
10949
|
}
|
|
10593
10950
|
}
|
|
10951
|
+
/**
|
|
10952
|
+
* Returns events that occurred after `lastEventId`.
|
|
10953
|
+
*
|
|
10954
|
+
* - If `lastEventId` is `undefined`, returns all buffered events (fresh connection).
|
|
10955
|
+
* - If `lastEventId` is not found in the buffer, returns `null` — the event has been
|
|
10956
|
+
* evicted and the client must be informed that a gap may exist.
|
|
10957
|
+
* - Otherwise returns the slice after the matching event.
|
|
10958
|
+
*/
|
|
10594
10959
|
getSince(sessionId, lastEventId) {
|
|
10595
10960
|
const buffer = this.buffers.get(sessionId);
|
|
10596
10961
|
if (!buffer || buffer.length === 0) return [];
|
|
@@ -10599,6 +10964,7 @@ var init_event_buffer = __esm({
|
|
|
10599
10964
|
if (index === -1) return null;
|
|
10600
10965
|
return buffer.slice(index + 1);
|
|
10601
10966
|
}
|
|
10967
|
+
/** Remove the buffer for a session — called when the session ends to free memory. */
|
|
10602
10968
|
cleanup(sessionId) {
|
|
10603
10969
|
this.buffers.delete(sessionId);
|
|
10604
10970
|
}
|
|
@@ -10680,6 +11046,12 @@ var init_adapter = __esm({
|
|
|
10680
11046
|
voice: false
|
|
10681
11047
|
};
|
|
10682
11048
|
heartbeatTimer;
|
|
11049
|
+
/**
|
|
11050
|
+
* Starts the heartbeat timer that keeps idle SSE connections alive.
|
|
11051
|
+
*
|
|
11052
|
+
* `.unref()` prevents the timer from blocking the Node.js event loop from
|
|
11053
|
+
* exiting if this is the only remaining async operation (e.g. during tests).
|
|
11054
|
+
*/
|
|
10683
11055
|
async start() {
|
|
10684
11056
|
this.heartbeatTimer = setInterval(() => {
|
|
10685
11057
|
const heartbeat = serializeHeartbeat();
|
|
@@ -10696,6 +11068,7 @@ var init_adapter = __esm({
|
|
|
10696
11068
|
this.heartbeatTimer.unref();
|
|
10697
11069
|
}
|
|
10698
11070
|
}
|
|
11071
|
+
/** Stops the heartbeat timer and closes all active connections. */
|
|
10699
11072
|
async stop() {
|
|
10700
11073
|
if (this.heartbeatTimer) {
|
|
10701
11074
|
clearInterval(this.heartbeatTimer);
|
|
@@ -10703,18 +11076,32 @@ var init_adapter = __esm({
|
|
|
10703
11076
|
}
|
|
10704
11077
|
this.connectionManager.cleanup();
|
|
10705
11078
|
}
|
|
11079
|
+
/**
|
|
11080
|
+
* Serializes an outgoing agent message, pushes it to the event buffer,
|
|
11081
|
+
* then broadcasts it to all active connections for the session.
|
|
11082
|
+
*
|
|
11083
|
+
* Buffering before broadcast ensures that a client reconnecting immediately
|
|
11084
|
+
* after this call can still replay the event via `Last-Event-ID`.
|
|
11085
|
+
*/
|
|
10706
11086
|
async sendMessage(sessionId, content) {
|
|
10707
11087
|
const eventId = generateEventId();
|
|
10708
11088
|
const serialized = serializeOutgoingMessage(sessionId, eventId, content);
|
|
10709
11089
|
this.eventBuffer.push(sessionId, { id: eventId, data: serialized });
|
|
10710
11090
|
this.connectionManager.broadcast(sessionId, serialized);
|
|
10711
11091
|
}
|
|
11092
|
+
/** Serializes and delivers a permission request UI to the session's SSE clients. */
|
|
10712
11093
|
async sendPermissionRequest(sessionId, request) {
|
|
10713
11094
|
const eventId = generateEventId();
|
|
10714
11095
|
const serialized = serializePermissionRequest(sessionId, eventId, request);
|
|
10715
11096
|
this.eventBuffer.push(sessionId, { id: eventId, data: serialized });
|
|
10716
11097
|
this.connectionManager.broadcast(sessionId, serialized);
|
|
10717
11098
|
}
|
|
11099
|
+
/**
|
|
11100
|
+
* Delivers a cross-session notification to the target session's SSE clients.
|
|
11101
|
+
*
|
|
11102
|
+
* Notifications are always buffered in addition to being broadcast so that
|
|
11103
|
+
* a client reconnecting shortly after (e.g. page refresh) still sees the alert.
|
|
11104
|
+
*/
|
|
10718
11105
|
async sendNotification(notification) {
|
|
10719
11106
|
if (notification.sessionId) {
|
|
10720
11107
|
const eventId = generateEventId();
|
|
@@ -10723,9 +11110,11 @@ var init_adapter = __esm({
|
|
|
10723
11110
|
this.connectionManager.broadcast(notification.sessionId, serialized);
|
|
10724
11111
|
}
|
|
10725
11112
|
}
|
|
11113
|
+
/** SSE has no concept of threads — return sessionId as the threadId */
|
|
10726
11114
|
async createSessionThread(sessionId, _name) {
|
|
10727
11115
|
return sessionId;
|
|
10728
11116
|
}
|
|
11117
|
+
/** No-op for SSE — there are no named threads to rename. */
|
|
10729
11118
|
async renameSessionThread(_sessionId, _newName) {
|
|
10730
11119
|
}
|
|
10731
11120
|
};
|
|
@@ -10765,6 +11154,7 @@ async function sseRoutes(app, deps) {
|
|
|
10765
11154
|
"Content-Type": "text/event-stream",
|
|
10766
11155
|
"Cache-Control": "no-cache",
|
|
10767
11156
|
"Connection": "keep-alive",
|
|
11157
|
+
// Disable buffering in Nginx/Cloudflare so events arrive without delay
|
|
10768
11158
|
"X-Accel-Buffering": "no"
|
|
10769
11159
|
});
|
|
10770
11160
|
raw.write(serializeConnected(connection.id, sessionId));
|
|
@@ -13694,13 +14084,16 @@ var init_settings_manager = __esm({
|
|
|
13694
14084
|
constructor(basePath) {
|
|
13695
14085
|
this.basePath = basePath;
|
|
13696
14086
|
}
|
|
14087
|
+
/** Returns the base path for all plugin settings directories. */
|
|
13697
14088
|
getBasePath() {
|
|
13698
14089
|
return this.basePath;
|
|
13699
14090
|
}
|
|
14091
|
+
/** Create a SettingsAPI instance scoped to a specific plugin. */
|
|
13700
14092
|
createAPI(pluginName) {
|
|
13701
14093
|
const settingsPath = this.getSettingsPath(pluginName);
|
|
13702
14094
|
return new SettingsAPIImpl(settingsPath);
|
|
13703
14095
|
}
|
|
14096
|
+
/** Load a plugin's settings from disk. Returns empty object if file doesn't exist. */
|
|
13704
14097
|
async loadSettings(pluginName) {
|
|
13705
14098
|
const settingsPath = this.getSettingsPath(pluginName);
|
|
13706
14099
|
try {
|
|
@@ -13710,6 +14103,7 @@ var init_settings_manager = __esm({
|
|
|
13710
14103
|
return {};
|
|
13711
14104
|
}
|
|
13712
14105
|
}
|
|
14106
|
+
/** Validate settings against a Zod schema. Returns valid if no schema is provided. */
|
|
13713
14107
|
validateSettings(_pluginName, settings, schema) {
|
|
13714
14108
|
if (!schema) return { valid: true };
|
|
13715
14109
|
const result = schema.safeParse(settings);
|
|
@@ -13721,12 +14115,14 @@ var init_settings_manager = __esm({
|
|
|
13721
14115
|
)
|
|
13722
14116
|
};
|
|
13723
14117
|
}
|
|
14118
|
+
/** Resolve the absolute path to a plugin's settings.json file. */
|
|
13724
14119
|
getSettingsPath(pluginName) {
|
|
13725
14120
|
return path30.join(this.basePath, pluginName, "settings.json");
|
|
13726
14121
|
}
|
|
13727
14122
|
async getPluginSettings(pluginName) {
|
|
13728
14123
|
return this.loadSettings(pluginName);
|
|
13729
14124
|
}
|
|
14125
|
+
/** Merge updates into existing settings (shallow merge). */
|
|
13730
14126
|
async updatePluginSettings(pluginName, updates) {
|
|
13731
14127
|
const api = this.createAPI(pluginName);
|
|
13732
14128
|
const current = await api.getAll();
|
|
@@ -14297,6 +14693,12 @@ var init_doctor = __esm({
|
|
|
14297
14693
|
this.dryRun = options?.dryRun ?? false;
|
|
14298
14694
|
this.dataDir = options.dataDir;
|
|
14299
14695
|
}
|
|
14696
|
+
/**
|
|
14697
|
+
* Executes all checks and returns an aggregated report.
|
|
14698
|
+
*
|
|
14699
|
+
* Safe fixes are applied inline (mutating CheckResult.message to show "Fixed").
|
|
14700
|
+
* Risky fixes are deferred to `report.pendingFixes` for user confirmation.
|
|
14701
|
+
*/
|
|
14300
14702
|
async runAll() {
|
|
14301
14703
|
const ctx = await this.buildContext();
|
|
14302
14704
|
const checks = [...ALL_CHECKS].sort((a, b) => a.order - b.order);
|
|
@@ -14344,6 +14746,7 @@ var init_doctor = __esm({
|
|
|
14344
14746
|
}
|
|
14345
14747
|
return { categories, summary, pendingFixes };
|
|
14346
14748
|
}
|
|
14749
|
+
/** Constructs the shared context used by all checks — loads config if available. */
|
|
14347
14750
|
async buildContext() {
|
|
14348
14751
|
const dataDir = this.dataDir;
|
|
14349
14752
|
const configPath = process.env.OPENACP_CONFIG_PATH || path35.join(dataDir, "config.json");
|
|
@@ -14991,6 +15394,14 @@ var init_permissions = __esm({
|
|
|
14991
15394
|
this.sendNotification = sendNotification;
|
|
14992
15395
|
}
|
|
14993
15396
|
pending = /* @__PURE__ */ new Map();
|
|
15397
|
+
/**
|
|
15398
|
+
* Send a permission request to the session's topic as an inline keyboard message,
|
|
15399
|
+
* and fire a notification to the Notifications topic so the user is alerted.
|
|
15400
|
+
*
|
|
15401
|
+
* Each button encodes `p:<callbackKey>:<optionId>`. The callbackKey is a short
|
|
15402
|
+
* nanoid that maps to the full pending state stored in-memory, avoiding the
|
|
15403
|
+
* 64-byte Telegram callback_data limit.
|
|
15404
|
+
*/
|
|
14994
15405
|
async sendPermissionRequest(session, request) {
|
|
14995
15406
|
const threadId = Number(session.threadId);
|
|
14996
15407
|
const callbackKey = nanoid3(8);
|
|
@@ -15025,6 +15436,12 @@ ${escapeHtml4(request.description)}`,
|
|
|
15025
15436
|
deepLink
|
|
15026
15437
|
});
|
|
15027
15438
|
}
|
|
15439
|
+
/**
|
|
15440
|
+
* Register the `p:` callback handler in the bot's middleware chain.
|
|
15441
|
+
*
|
|
15442
|
+
* Must be called during setup so grammY processes permission responses
|
|
15443
|
+
* before other generic callback handlers.
|
|
15444
|
+
*/
|
|
15028
15445
|
setupCallbackHandler() {
|
|
15029
15446
|
this.bot.on("callback_query:data", async (ctx, next) => {
|
|
15030
15447
|
const data = ctx.callbackQuery.data;
|
|
@@ -15072,6 +15489,9 @@ var init_tool_card_state = __esm({
|
|
|
15072
15489
|
specs = [];
|
|
15073
15490
|
planEntries;
|
|
15074
15491
|
usage;
|
|
15492
|
+
// Lifecycle: active (first flush pending) → active (subsequent updates debounced) → finalized.
|
|
15493
|
+
// Once finalized, all updateFromSpec/updatePlan/appendUsage/finalize() calls are no-ops —
|
|
15494
|
+
// guards against events arriving after the session has ended or the tool has already completed.
|
|
15075
15495
|
finalized = false;
|
|
15076
15496
|
isFirstFlush = true;
|
|
15077
15497
|
debounceTimer;
|
|
@@ -15079,6 +15499,7 @@ var init_tool_card_state = __esm({
|
|
|
15079
15499
|
constructor(config) {
|
|
15080
15500
|
this.onFlush = config.onFlush;
|
|
15081
15501
|
}
|
|
15502
|
+
/** Adds or updates a tool spec. First call flushes immediately; subsequent calls are debounced. */
|
|
15082
15503
|
updateFromSpec(spec) {
|
|
15083
15504
|
if (this.finalized) return;
|
|
15084
15505
|
const existingIdx = this.specs.findIndex((s) => s.id === spec.id);
|
|
@@ -15094,6 +15515,7 @@ var init_tool_card_state = __esm({
|
|
|
15094
15515
|
this.scheduleFlush();
|
|
15095
15516
|
}
|
|
15096
15517
|
}
|
|
15518
|
+
/** Updates the plan entries displayed alongside tool cards. */
|
|
15097
15519
|
updatePlan(entries) {
|
|
15098
15520
|
if (this.finalized) return;
|
|
15099
15521
|
this.planEntries = entries;
|
|
@@ -15104,17 +15526,20 @@ var init_tool_card_state = __esm({
|
|
|
15104
15526
|
this.scheduleFlush();
|
|
15105
15527
|
}
|
|
15106
15528
|
}
|
|
15529
|
+
/** Appends token usage data to the tool card (typically at end of turn). */
|
|
15107
15530
|
appendUsage(usage) {
|
|
15108
15531
|
if (this.finalized) return;
|
|
15109
15532
|
this.usage = usage;
|
|
15110
15533
|
this.scheduleFlush();
|
|
15111
15534
|
}
|
|
15535
|
+
/** Marks the turn as complete and flushes the final snapshot immediately. */
|
|
15112
15536
|
finalize() {
|
|
15113
15537
|
if (this.finalized) return;
|
|
15114
15538
|
this.finalized = true;
|
|
15115
15539
|
this.clearDebounce();
|
|
15116
15540
|
this.flush();
|
|
15117
15541
|
}
|
|
15542
|
+
/** Stops all pending flushes without emitting a final snapshot. */
|
|
15118
15543
|
destroy() {
|
|
15119
15544
|
this.finalized = true;
|
|
15120
15545
|
this.clearDebounce();
|
|
@@ -15220,9 +15645,11 @@ var init_stream_accumulator = __esm({
|
|
|
15220
15645
|
entry.diffStats = update.diffStats;
|
|
15221
15646
|
}
|
|
15222
15647
|
}
|
|
15648
|
+
/** Retrieves a tool entry by ID, or undefined if not yet tracked. */
|
|
15223
15649
|
get(id) {
|
|
15224
15650
|
return this.entries.get(id);
|
|
15225
15651
|
}
|
|
15652
|
+
/** Resets all state between turns. */
|
|
15226
15653
|
clear() {
|
|
15227
15654
|
this.entries.clear();
|
|
15228
15655
|
this.pendingUpdates.clear();
|
|
@@ -15231,20 +15658,24 @@ var init_stream_accumulator = __esm({
|
|
|
15231
15658
|
ThoughtBuffer = class {
|
|
15232
15659
|
chunks = [];
|
|
15233
15660
|
sealed = false;
|
|
15661
|
+
/** Appends a thought text chunk. Ignored if already sealed. */
|
|
15234
15662
|
append(chunk) {
|
|
15235
15663
|
if (this.sealed) return;
|
|
15236
15664
|
this.chunks.push(chunk);
|
|
15237
15665
|
}
|
|
15666
|
+
/** Marks the thought as complete and returns the full accumulated text. */
|
|
15238
15667
|
seal() {
|
|
15239
15668
|
this.sealed = true;
|
|
15240
15669
|
return this.chunks.join("");
|
|
15241
15670
|
}
|
|
15671
|
+
/** Returns the text accumulated so far without sealing. */
|
|
15242
15672
|
getText() {
|
|
15243
15673
|
return this.chunks.join("");
|
|
15244
15674
|
}
|
|
15245
15675
|
isSealed() {
|
|
15246
15676
|
return this.sealed;
|
|
15247
15677
|
}
|
|
15678
|
+
/** Resets the buffer for reuse in a new turn. */
|
|
15248
15679
|
reset() {
|
|
15249
15680
|
this.chunks = [];
|
|
15250
15681
|
this.sealed = false;
|
|
@@ -15416,6 +15847,13 @@ var init_display_spec_builder = __esm({
|
|
|
15416
15847
|
constructor(tunnelService) {
|
|
15417
15848
|
this.tunnelService = tunnelService;
|
|
15418
15849
|
}
|
|
15850
|
+
/**
|
|
15851
|
+
* Builds a display spec for a single tool call entry.
|
|
15852
|
+
*
|
|
15853
|
+
* Deduplicates fields to avoid repeating the same info (e.g., if the title
|
|
15854
|
+
* was derived from the command, the command field is omitted). For long
|
|
15855
|
+
* output, generates a viewer link via the tunnel service when available.
|
|
15856
|
+
*/
|
|
15419
15857
|
buildToolSpec(entry, mode, sessionContext) {
|
|
15420
15858
|
const effectiveKind = entry.displayKind ?? (isApplyPatchOtherTool(entry.kind, entry.name, entry.rawInput) ? "edit" : entry.kind);
|
|
15421
15859
|
const icon = KIND_ICONS[effectiveKind] ?? KIND_ICONS["other"] ?? "\u{1F6E0}\uFE0F";
|
|
@@ -15474,6 +15912,7 @@ var init_display_spec_builder = __esm({
|
|
|
15474
15912
|
isHidden
|
|
15475
15913
|
};
|
|
15476
15914
|
}
|
|
15915
|
+
/** Builds a display spec for an agent thought. Content is only included at high verbosity. */
|
|
15477
15916
|
buildThoughtSpec(content, mode) {
|
|
15478
15917
|
const indicator = "Thinking...";
|
|
15479
15918
|
return {
|
|
@@ -15607,7 +16046,10 @@ var init_activity = __esm({
|
|
|
15607
16046
|
this.sessionId = sessionId;
|
|
15608
16047
|
this.state = new ToolCardState({
|
|
15609
16048
|
onFlush: (snapshot) => {
|
|
15610
|
-
this.flushPromise = this.flushPromise.then(() =>
|
|
16049
|
+
this.flushPromise = this.flushPromise.then(() => {
|
|
16050
|
+
if (this.aborted) return;
|
|
16051
|
+
return this._sendOrEdit(snapshot);
|
|
16052
|
+
}).catch(() => {
|
|
15611
16053
|
});
|
|
15612
16054
|
}
|
|
15613
16055
|
});
|
|
@@ -15617,6 +16059,7 @@ var init_activity = __esm({
|
|
|
15617
16059
|
lastSentText;
|
|
15618
16060
|
flushPromise = Promise.resolve();
|
|
15619
16061
|
overflowMsgIds = [];
|
|
16062
|
+
aborted = false;
|
|
15620
16063
|
tracer;
|
|
15621
16064
|
sessionId;
|
|
15622
16065
|
updateFromSpec(spec) {
|
|
@@ -15633,6 +16076,7 @@ var init_activity = __esm({
|
|
|
15633
16076
|
await this.flushPromise;
|
|
15634
16077
|
}
|
|
15635
16078
|
destroy() {
|
|
16079
|
+
this.aborted = true;
|
|
15636
16080
|
this.state.destroy();
|
|
15637
16081
|
}
|
|
15638
16082
|
hasContent() {
|
|
@@ -15864,6 +16308,13 @@ var init_send_queue = __esm({
|
|
|
15864
16308
|
get pending() {
|
|
15865
16309
|
return this.items.length;
|
|
15866
16310
|
}
|
|
16311
|
+
/**
|
|
16312
|
+
* Queues an async operation for rate-limited execution.
|
|
16313
|
+
*
|
|
16314
|
+
* For text-type items with a key, replaces any existing queued item with
|
|
16315
|
+
* the same key (deduplication). This is used for streaming draft updates
|
|
16316
|
+
* where only the latest content matters.
|
|
16317
|
+
*/
|
|
15867
16318
|
enqueue(fn, opts) {
|
|
15868
16319
|
const type = opts?.type ?? "other";
|
|
15869
16320
|
const key = opts?.key;
|
|
@@ -15891,6 +16342,11 @@ var init_send_queue = __esm({
|
|
|
15891
16342
|
this.scheduleProcess();
|
|
15892
16343
|
return promise;
|
|
15893
16344
|
}
|
|
16345
|
+
/**
|
|
16346
|
+
* Called when a platform rate limit is hit. Drops all pending text items
|
|
16347
|
+
* (draft updates) to reduce backlog, keeping only non-text items that
|
|
16348
|
+
* represent important operations (e.g., permission requests).
|
|
16349
|
+
*/
|
|
15894
16350
|
onRateLimited() {
|
|
15895
16351
|
this.config.onRateLimited?.();
|
|
15896
16352
|
const remaining2 = [];
|
|
@@ -15909,6 +16365,10 @@ var init_send_queue = __esm({
|
|
|
15909
16365
|
}
|
|
15910
16366
|
this.items = [];
|
|
15911
16367
|
}
|
|
16368
|
+
/**
|
|
16369
|
+
* Schedules the next item for processing after the rate-limit delay.
|
|
16370
|
+
* Uses per-category timing when available, falling back to the global minInterval.
|
|
16371
|
+
*/
|
|
15912
16372
|
scheduleProcess() {
|
|
15913
16373
|
if (this.processing) return;
|
|
15914
16374
|
if (this.items.length === 0) return;
|
|
@@ -15981,6 +16441,7 @@ var init_streaming = __esm({
|
|
|
15981
16441
|
lastSentHtml = "";
|
|
15982
16442
|
displayTruncated = false;
|
|
15983
16443
|
tracer;
|
|
16444
|
+
/** Append a text chunk to the buffer and schedule a throttled flush. */
|
|
15984
16445
|
append(text5) {
|
|
15985
16446
|
if (!text5) return;
|
|
15986
16447
|
this.buffer += text5;
|
|
@@ -16060,6 +16521,16 @@ var init_streaming = __esm({
|
|
|
16060
16521
|
}
|
|
16061
16522
|
}
|
|
16062
16523
|
}
|
|
16524
|
+
/**
|
|
16525
|
+
* Flush the complete buffer as the final message for this turn.
|
|
16526
|
+
*
|
|
16527
|
+
* Cancels any pending debounce timer, waits for in-flight flushes to settle,
|
|
16528
|
+
* then sends the full content. If the HTML exceeds 4096 bytes, the content is
|
|
16529
|
+
* split at markdown boundaries to avoid breaking HTML tags mid-tag.
|
|
16530
|
+
*
|
|
16531
|
+
* Returns the Telegram message ID of the sent/edited message, or undefined
|
|
16532
|
+
* if nothing was sent (empty buffer or all network calls failed).
|
|
16533
|
+
*/
|
|
16063
16534
|
async finalize() {
|
|
16064
16535
|
this.tracer?.log("telegram", { action: "draft:finalize", sessionId: this.sessionId, bufferLen: this.buffer.length, msgId: this.messageId });
|
|
16065
16536
|
if (this.flushTimer) {
|
|
@@ -16131,9 +16602,14 @@ var init_streaming = __esm({
|
|
|
16131
16602
|
this.tracer?.log("telegram", { action: "draft:finalize:split", sessionId: this.sessionId, chunks: mdChunks.length });
|
|
16132
16603
|
return this.messageId;
|
|
16133
16604
|
}
|
|
16605
|
+
/** Returns the Telegram message ID for this draft, or undefined if not yet sent. */
|
|
16134
16606
|
getMessageId() {
|
|
16135
16607
|
return this.messageId;
|
|
16136
16608
|
}
|
|
16609
|
+
/**
|
|
16610
|
+
* Strip occurrences of `pattern` from the buffer and edit the message in-place.
|
|
16611
|
+
* Used by the TTS plugin to remove [TTS]...[/TTS] blocks after audio is sent.
|
|
16612
|
+
*/
|
|
16137
16613
|
async stripPattern(pattern) {
|
|
16138
16614
|
if (!this.messageId || !this.buffer) return;
|
|
16139
16615
|
const stripped = this.buffer.replace(pattern, "").trim();
|
|
@@ -16171,6 +16647,10 @@ var init_draft_manager = __esm({
|
|
|
16171
16647
|
drafts = /* @__PURE__ */ new Map();
|
|
16172
16648
|
textBuffers = /* @__PURE__ */ new Map();
|
|
16173
16649
|
finalizedDrafts = /* @__PURE__ */ new Map();
|
|
16650
|
+
/**
|
|
16651
|
+
* Return the active draft for a session, creating one if it doesn't exist yet.
|
|
16652
|
+
* Only one draft per session exists at a time.
|
|
16653
|
+
*/
|
|
16174
16654
|
getOrCreate(sessionId, threadId, tracer = null) {
|
|
16175
16655
|
let draft = this.drafts.get(sessionId);
|
|
16176
16656
|
if (!draft) {
|
|
@@ -16199,7 +16679,12 @@ var init_draft_manager = __esm({
|
|
|
16199
16679
|
);
|
|
16200
16680
|
}
|
|
16201
16681
|
/**
|
|
16202
|
-
* Finalize the
|
|
16682
|
+
* Finalize the active draft for a session and retain a short-lived reference for post-send edits.
|
|
16683
|
+
*
|
|
16684
|
+
* Removes the draft from the active map before awaiting to prevent concurrent calls from
|
|
16685
|
+
* double-finalizing the same draft. If the draft produces a message ID, stores it as a
|
|
16686
|
+
* `FinalizedDraft` so `stripPattern` (e.g. TTS block removal) can still edit the message
|
|
16687
|
+
* after it has been sent.
|
|
16203
16688
|
*/
|
|
16204
16689
|
async finalize(sessionId, _assistantSessionId) {
|
|
16205
16690
|
const draft = this.drafts.get(sessionId);
|
|
@@ -16226,6 +16711,12 @@ var init_draft_manager = __esm({
|
|
|
16226
16711
|
await finalized.draft.stripPattern(pattern);
|
|
16227
16712
|
}
|
|
16228
16713
|
}
|
|
16714
|
+
/**
|
|
16715
|
+
* Discard all draft state for a session without sending anything.
|
|
16716
|
+
*
|
|
16717
|
+
* Removes the active draft, text buffer, and finalized draft reference. Called when a
|
|
16718
|
+
* session ends or is reset and any unsent content should be silently dropped.
|
|
16719
|
+
*/
|
|
16229
16720
|
cleanup(sessionId) {
|
|
16230
16721
|
this.drafts.delete(sessionId);
|
|
16231
16722
|
this.textBuffers.delete(sessionId);
|
|
@@ -16244,14 +16735,19 @@ var init_skill_command_manager = __esm({
|
|
|
16244
16735
|
init_log();
|
|
16245
16736
|
log28 = createChildLogger({ module: "skill-commands" });
|
|
16246
16737
|
SkillCommandManager = class {
|
|
16247
|
-
// sessionId → pinned msgId
|
|
16248
16738
|
constructor(bot, chatId, sendQueue, sessionManager) {
|
|
16249
16739
|
this.bot = bot;
|
|
16250
16740
|
this.chatId = chatId;
|
|
16251
16741
|
this.sendQueue = sendQueue;
|
|
16252
16742
|
this.sessionManager = sessionManager;
|
|
16253
16743
|
}
|
|
16744
|
+
// sessionId → Telegram message ID of the pinned skills message
|
|
16254
16745
|
messages = /* @__PURE__ */ new Map();
|
|
16746
|
+
/**
|
|
16747
|
+
* Send or update the pinned skill commands message for a session.
|
|
16748
|
+
* Creates a new pinned message if none exists; edits the existing one otherwise.
|
|
16749
|
+
* Passing an empty `commands` array removes the pinned message.
|
|
16750
|
+
*/
|
|
16255
16751
|
async send(sessionId, threadId, commands) {
|
|
16256
16752
|
if (!this.messages.has(sessionId)) {
|
|
16257
16753
|
const record = this.sessionManager.getSessionRecord(sessionId);
|
|
@@ -16351,11 +16847,21 @@ var init_messaging_adapter = __esm({
|
|
|
16351
16847
|
this.adapterConfig = adapterConfig;
|
|
16352
16848
|
}
|
|
16353
16849
|
// === Message dispatch flow ===
|
|
16850
|
+
/**
|
|
16851
|
+
* Entry point for all outbound messages from sessions to the platform.
|
|
16852
|
+
* Resolves the current verbosity, filters messages that should be hidden,
|
|
16853
|
+
* then dispatches to the appropriate type-specific handler.
|
|
16854
|
+
*/
|
|
16354
16855
|
async sendMessage(sessionId, content) {
|
|
16355
16856
|
const verbosity = this.getVerbosity();
|
|
16356
16857
|
if (!this.shouldDisplay(content, verbosity)) return;
|
|
16357
16858
|
await this.dispatchMessage(sessionId, content, verbosity);
|
|
16358
16859
|
}
|
|
16860
|
+
/**
|
|
16861
|
+
* Routes a message to its type-specific handler.
|
|
16862
|
+
* Subclasses can override this for custom dispatch logic, but typically
|
|
16863
|
+
* override individual handle* methods instead.
|
|
16864
|
+
*/
|
|
16359
16865
|
async dispatchMessage(sessionId, content, verbosity) {
|
|
16360
16866
|
switch (content.type) {
|
|
16361
16867
|
case "text":
|
|
@@ -16393,6 +16899,8 @@ var init_messaging_adapter = __esm({
|
|
|
16393
16899
|
}
|
|
16394
16900
|
}
|
|
16395
16901
|
// === Default handlers — all protected, all overridable ===
|
|
16902
|
+
// Each handler is a no-op by default. Subclasses override only the message
|
|
16903
|
+
// types they support (e.g., Telegram overrides handleText, handleToolCall, etc.).
|
|
16396
16904
|
async handleText(_sessionId, _content) {
|
|
16397
16905
|
}
|
|
16398
16906
|
async handleThought(_sessionId, _content, _verbosity) {
|
|
@@ -16426,6 +16934,10 @@ var init_messaging_adapter = __esm({
|
|
|
16426
16934
|
async handleResourceLink(_sessionId, _content) {
|
|
16427
16935
|
}
|
|
16428
16936
|
// === Helpers ===
|
|
16937
|
+
/**
|
|
16938
|
+
* Resolves the current output verbosity by checking (in priority order):
|
|
16939
|
+
* per-channel config, global config, then adapter default. Falls back to "medium".
|
|
16940
|
+
*/
|
|
16429
16941
|
getVerbosity() {
|
|
16430
16942
|
const config = this.context.configManager.get();
|
|
16431
16943
|
const channelConfig = config.channels;
|
|
@@ -16434,6 +16946,13 @@ var init_messaging_adapter = __esm({
|
|
|
16434
16946
|
if (v === "low" || v === "high") return v;
|
|
16435
16947
|
return "medium";
|
|
16436
16948
|
}
|
|
16949
|
+
/**
|
|
16950
|
+
* Determines whether a message should be displayed at the given verbosity.
|
|
16951
|
+
*
|
|
16952
|
+
* Noise filtering: tool calls matching noise rules (e.g., `ls`, `glob`, `grep`)
|
|
16953
|
+
* are hidden at medium/low verbosity to reduce clutter. Thoughts and usage
|
|
16954
|
+
* stats are hidden entirely at "low".
|
|
16955
|
+
*/
|
|
16437
16956
|
shouldDisplay(content, verbosity) {
|
|
16438
16957
|
if (verbosity === "low" && HIDDEN_ON_LOW.has(content.type)) return false;
|
|
16439
16958
|
if (content.type === "tool_call") {
|
|
@@ -16665,6 +17184,7 @@ var init_output_mode_resolver = __esm({
|
|
|
16665
17184
|
"use strict";
|
|
16666
17185
|
VALID_MODES = /* @__PURE__ */ new Set(["low", "medium", "high"]);
|
|
16667
17186
|
OutputModeResolver = class {
|
|
17187
|
+
/** Resolves the effective output mode by walking the override cascade. */
|
|
16668
17188
|
resolve(configManager, adapterName, sessionId, sessionManager) {
|
|
16669
17189
|
const config = configManager.get();
|
|
16670
17190
|
let mode = toOutputMode(config.outputMode) ?? "medium";
|
|
@@ -16763,7 +17283,11 @@ var init_adapter2 = __esm({
|
|
|
16763
17283
|
_topicsInitialized = false;
|
|
16764
17284
|
/** Background watcher timer — cancelled on stop() or when topics succeed */
|
|
16765
17285
|
_prerequisiteWatcher = null;
|
|
16766
|
-
/**
|
|
17286
|
+
/**
|
|
17287
|
+
* Persist the control message ID both in-memory and to the session record.
|
|
17288
|
+
* The control message is the pinned status card with bypass/TTS buttons; its ID
|
|
17289
|
+
* is needed after a restart to edit it when config changes.
|
|
17290
|
+
*/
|
|
16767
17291
|
storeControlMsgId(sessionId, msgId) {
|
|
16768
17292
|
this.controlMsgIds.set(sessionId, msgId);
|
|
16769
17293
|
const record = this.core.sessionManager.getSessionRecord(sessionId);
|
|
@@ -16828,6 +17352,12 @@ var init_adapter2 = __esm({
|
|
|
16828
17352
|
this.telegramConfig = config;
|
|
16829
17353
|
this.saveTopicIds = saveTopicIds;
|
|
16830
17354
|
}
|
|
17355
|
+
/**
|
|
17356
|
+
* Set up the grammY bot, register all callback and message handlers, then perform
|
|
17357
|
+
* two-phase startup: Phase 1 starts polling immediately; Phase 2 checks group
|
|
17358
|
+
* prerequisites (bot is admin, topics are enabled) and creates/restores system topics.
|
|
17359
|
+
* If prerequisites are not met, a background watcher retries until they are.
|
|
17360
|
+
*/
|
|
16831
17361
|
async start() {
|
|
16832
17362
|
this.bot = new Bot(this.telegramConfig.botToken, {
|
|
16833
17363
|
client: {
|
|
@@ -17258,6 +17788,13 @@ OpenACP will automatically retry until this is resolved.`;
|
|
|
17258
17788
|
};
|
|
17259
17789
|
this._prerequisiteWatcher = setTimeout(retry, schedule[0]);
|
|
17260
17790
|
}
|
|
17791
|
+
/**
|
|
17792
|
+
* Tear down the bot and release all associated resources.
|
|
17793
|
+
*
|
|
17794
|
+
* Cancels the background prerequisite watcher, destroys all per-session activity
|
|
17795
|
+
* trackers (which hold interval timers), removes eventBus listeners, clears the
|
|
17796
|
+
* send queue, and stops the grammY bot polling loop.
|
|
17797
|
+
*/
|
|
17261
17798
|
async stop() {
|
|
17262
17799
|
if (this._prerequisiteWatcher !== null) {
|
|
17263
17800
|
clearTimeout(this._prerequisiteWatcher);
|
|
@@ -17391,8 +17928,7 @@ ${lines.join("\n")}`;
|
|
|
17391
17928
|
await this.draftManager.finalize(sessionId, assistantSession?.id);
|
|
17392
17929
|
}
|
|
17393
17930
|
if (sessionId) {
|
|
17394
|
-
|
|
17395
|
-
if (tracker) await tracker.onNewPrompt();
|
|
17931
|
+
await this.drainAndResetTracker(sessionId);
|
|
17396
17932
|
}
|
|
17397
17933
|
ctx.replyWithChatAction("typing").catch(() => {
|
|
17398
17934
|
});
|
|
@@ -17481,9 +18017,26 @@ ${lines.join("\n")}`;
|
|
|
17481
18017
|
* its creation event. This queue ensures events are processed in the order they arrive.
|
|
17482
18018
|
*/
|
|
17483
18019
|
_dispatchQueues = /* @__PURE__ */ new Map();
|
|
18020
|
+
/**
|
|
18021
|
+
* Drain pending event dispatches from the previous prompt, then reset the
|
|
18022
|
+
* activity tracker so late tool_call events don't leak into the new card.
|
|
18023
|
+
*/
|
|
18024
|
+
async drainAndResetTracker(sessionId) {
|
|
18025
|
+
const pendingDispatch = this._dispatchQueues.get(sessionId);
|
|
18026
|
+
if (pendingDispatch) await pendingDispatch;
|
|
18027
|
+
const tracker = this.sessionTrackers.get(sessionId);
|
|
18028
|
+
if (tracker) await tracker.onNewPrompt();
|
|
18029
|
+
}
|
|
17484
18030
|
getTracer(sessionId) {
|
|
17485
18031
|
return this.core.sessionManager.getSession(sessionId)?.agentInstance?.debugTracer ?? null;
|
|
17486
18032
|
}
|
|
18033
|
+
/**
|
|
18034
|
+
* Primary outbound dispatch method — routes an agent message to the session's Telegram topic.
|
|
18035
|
+
*
|
|
18036
|
+
* Wraps the base class `sendMessage` in a per-session promise chain (`_dispatchQueues`)
|
|
18037
|
+
* so that concurrent events fired from SessionBridge are serialized and delivered in the
|
|
18038
|
+
* order they arrive, preventing fast handlers from overtaking slower ones.
|
|
18039
|
+
*/
|
|
17487
18040
|
async sendMessage(sessionId, content) {
|
|
17488
18041
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
17489
18042
|
if (!session) return;
|
|
@@ -17797,6 +18350,11 @@ Task completed.
|
|
|
17797
18350
|
)
|
|
17798
18351
|
);
|
|
17799
18352
|
}
|
|
18353
|
+
/**
|
|
18354
|
+
* Render a PermissionRequest as an inline keyboard in the session topic and
|
|
18355
|
+
* notify the Notifications topic. Runs inside a sendQueue item, so
|
|
18356
|
+
* notification is fire-and-forget to avoid deadlock.
|
|
18357
|
+
*/
|
|
17800
18358
|
async sendPermissionRequest(sessionId, request) {
|
|
17801
18359
|
this.getTracer(sessionId)?.log("telegram", { action: "permission:send", sessionId, requestId: request.id, description: request.description });
|
|
17802
18360
|
log29.info({ sessionId, requestId: request.id }, "Permission request sent");
|
|
@@ -17806,6 +18364,11 @@ Task completed.
|
|
|
17806
18364
|
() => this.permissionHandler.sendPermissionRequest(session, request)
|
|
17807
18365
|
);
|
|
17808
18366
|
}
|
|
18367
|
+
/**
|
|
18368
|
+
* Post a notification to the Notifications topic.
|
|
18369
|
+
* Assistant session notifications are suppressed — the assistant topic is
|
|
18370
|
+
* the user's primary interface and does not need a separate alert.
|
|
18371
|
+
*/
|
|
17809
18372
|
async sendNotification(notification) {
|
|
17810
18373
|
this.getTracer(notification.sessionId)?.log("telegram", { action: "notification:send", sessionId: notification.sessionId, type: notification.type });
|
|
17811
18374
|
if (notification.sessionId === this.core.assistantManager?.get("telegram")?.id) return;
|
|
@@ -17846,6 +18409,10 @@ Task completed.
|
|
|
17846
18409
|
})
|
|
17847
18410
|
);
|
|
17848
18411
|
}
|
|
18412
|
+
/**
|
|
18413
|
+
* Create a new Telegram forum topic for a session and return its thread ID as a string.
|
|
18414
|
+
* Called by the core when a session is created via the API or CLI (not from the Telegram UI).
|
|
18415
|
+
*/
|
|
17849
18416
|
async createSessionThread(sessionId, name) {
|
|
17850
18417
|
this.getTracer(sessionId)?.log("telegram", { action: "thread:create", sessionId, name });
|
|
17851
18418
|
log29.info({ sessionId, name }, "Session topic created");
|
|
@@ -17853,6 +18420,10 @@ Task completed.
|
|
|
17853
18420
|
await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
|
|
17854
18421
|
);
|
|
17855
18422
|
}
|
|
18423
|
+
/**
|
|
18424
|
+
* Rename the forum topic for a session and update the session record's display name.
|
|
18425
|
+
* No-ops silently if the session doesn't have a threadId yet (e.g. still initializing).
|
|
18426
|
+
*/
|
|
17856
18427
|
async renameSessionThread(sessionId, newName) {
|
|
17857
18428
|
this.getTracer(sessionId)?.log("telegram", { action: "thread:rename", sessionId, newName });
|
|
17858
18429
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
@@ -17870,6 +18441,7 @@ Task completed.
|
|
|
17870
18441
|
);
|
|
17871
18442
|
await this.core.sessionManager.patchRecord(sessionId, { name: newName });
|
|
17872
18443
|
}
|
|
18444
|
+
/** Delete the forum topic associated with a session. */
|
|
17873
18445
|
async deleteSessionThread(sessionId) {
|
|
17874
18446
|
const record = this.core.sessionManager.getSessionRecord(sessionId);
|
|
17875
18447
|
const platform2 = record?.platform;
|
|
@@ -17884,6 +18456,11 @@ Task completed.
|
|
|
17884
18456
|
);
|
|
17885
18457
|
}
|
|
17886
18458
|
}
|
|
18459
|
+
/**
|
|
18460
|
+
* Display or update the pinned skill commands message for a session.
|
|
18461
|
+
* If the session's threadId is not yet set (e.g. session created from API),
|
|
18462
|
+
* the commands are queued and flushed once the thread becomes available.
|
|
18463
|
+
*/
|
|
17887
18464
|
async sendSkillCommands(sessionId, commands) {
|
|
17888
18465
|
if (sessionId === this.core.assistantManager?.get("telegram")?.id) return;
|
|
17889
18466
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
@@ -17968,7 +18545,10 @@ Task completed.
|
|
|
17968
18545
|
return;
|
|
17969
18546
|
}
|
|
17970
18547
|
const sid = await this.resolveSessionId(threadId);
|
|
17971
|
-
if (sid)
|
|
18548
|
+
if (sid) {
|
|
18549
|
+
await this.draftManager.finalize(sid, this.core.assistantManager?.get("telegram")?.id);
|
|
18550
|
+
await this.drainAndResetTracker(sid);
|
|
18551
|
+
}
|
|
17972
18552
|
this.core.handleMessage({
|
|
17973
18553
|
channelId: "telegram",
|
|
17974
18554
|
threadId: String(threadId),
|
|
@@ -17977,10 +18557,24 @@ Task completed.
|
|
|
17977
18557
|
attachments: [att]
|
|
17978
18558
|
}).catch((err) => log29.error({ err }, "handleMessage error"));
|
|
17979
18559
|
}
|
|
18560
|
+
/**
|
|
18561
|
+
* Remove skill slash commands from the Telegram bot command list for a session.
|
|
18562
|
+
*
|
|
18563
|
+
* Clears any queued pending commands that hadn't been sent yet, then delegates
|
|
18564
|
+
* to `SkillCommandManager` to delete the commands from the Telegram API. Called
|
|
18565
|
+
* when a session with registered skill commands ends.
|
|
18566
|
+
*/
|
|
17980
18567
|
async cleanupSkillCommands(sessionId) {
|
|
17981
18568
|
this._pendingSkillCommands.delete(sessionId);
|
|
17982
18569
|
await this.skillManager.cleanup(sessionId);
|
|
17983
18570
|
}
|
|
18571
|
+
/**
|
|
18572
|
+
* Clean up all adapter state associated with a session.
|
|
18573
|
+
*
|
|
18574
|
+
* Finalizes and discards any in-flight draft, destroys the activity tracker
|
|
18575
|
+
* (stopping ThinkingIndicator timers and finalizing any open ToolCard), and
|
|
18576
|
+
* clears pending skill commands. Called when a session ends or is reset.
|
|
18577
|
+
*/
|
|
17984
18578
|
async cleanupSessionState(sessionId) {
|
|
17985
18579
|
this._pendingSkillCommands.delete(sessionId);
|
|
17986
18580
|
await this.draftManager.finalize(sessionId, this.core.assistantManager?.get("telegram")?.id);
|
|
@@ -17991,9 +18585,22 @@ Task completed.
|
|
|
17991
18585
|
this.sessionTrackers.delete(sessionId);
|
|
17992
18586
|
}
|
|
17993
18587
|
}
|
|
18588
|
+
/**
|
|
18589
|
+
* Remove `[TTS]...[/TTS]` blocks from the active or finalized draft for a session.
|
|
18590
|
+
*
|
|
18591
|
+
* The agent embeds these blocks so the speech plugin can extract the TTS text, but
|
|
18592
|
+
* they should never appear in the chat message. Called after TTS audio has been sent.
|
|
18593
|
+
*/
|
|
17994
18594
|
async stripTTSBlock(sessionId) {
|
|
17995
18595
|
await this.draftManager.stripPattern(sessionId, /\[TTS\][\s\S]*?\[\/TTS\]/g);
|
|
17996
18596
|
}
|
|
18597
|
+
/**
|
|
18598
|
+
* Archive a session by deleting its forum topic.
|
|
18599
|
+
*
|
|
18600
|
+
* Sets `session.archiving = true` to suppress any outgoing messages while the
|
|
18601
|
+
* topic is being torn down, finalizes pending drafts, cleans up all trackers,
|
|
18602
|
+
* then deletes the Telegram topic (which removes all messages).
|
|
18603
|
+
*/
|
|
17997
18604
|
async archiveSessionTopic(sessionId) {
|
|
17998
18605
|
this.getTracer(sessionId)?.log("telegram", { action: "thread:archive", sessionId });
|
|
17999
18606
|
const core = this.core;
|
|
@@ -19022,13 +19629,18 @@ var init_path_guard = __esm({
|
|
|
19022
19629
|
"src/core/security/path-guard.ts"() {
|
|
19023
19630
|
"use strict";
|
|
19024
19631
|
DEFAULT_DENY_PATTERNS = [
|
|
19632
|
+
// Environment files — contain API keys, database URLs, etc.
|
|
19025
19633
|
".env",
|
|
19026
19634
|
".env.*",
|
|
19635
|
+
// Cryptographic keys
|
|
19027
19636
|
"*.key",
|
|
19028
19637
|
"*.pem",
|
|
19638
|
+
// SSH and cloud credentials
|
|
19029
19639
|
".ssh/",
|
|
19030
19640
|
".aws/",
|
|
19641
|
+
// OpenACP workspace — contains bot tokens and secrets
|
|
19031
19642
|
".openacp/",
|
|
19643
|
+
// Generic credential/secret files
|
|
19032
19644
|
"**/credentials*",
|
|
19033
19645
|
"**/secrets*",
|
|
19034
19646
|
"**/*.secret"
|
|
@@ -19056,6 +19668,20 @@ var init_path_guard = __esm({
|
|
|
19056
19668
|
this.ig.add(options.ignorePatterns);
|
|
19057
19669
|
}
|
|
19058
19670
|
}
|
|
19671
|
+
/**
|
|
19672
|
+
* Checks whether an agent is allowed to access the given path.
|
|
19673
|
+
*
|
|
19674
|
+
* Validation order:
|
|
19675
|
+
* 1. Write to .openacpignore is always blocked (prevents agents from weakening their own restrictions)
|
|
19676
|
+
* 2. Path must be within cwd or an explicitly allowlisted path
|
|
19677
|
+
* 3. If within cwd but not allowlisted, path must not match any deny pattern
|
|
19678
|
+
*
|
|
19679
|
+
* @param targetPath - The path the agent is attempting to access.
|
|
19680
|
+
* @param operation - The operation type. Write operations are subject to stricter
|
|
19681
|
+
* restrictions than reads — specifically, writing to `.openacpignore` is blocked
|
|
19682
|
+
* (to prevent agents from weakening their own restrictions), while reading it is allowed.
|
|
19683
|
+
* @returns `{ allowed: true }` or `{ allowed: false, reason: "..." }`
|
|
19684
|
+
*/
|
|
19059
19685
|
validatePath(targetPath, operation) {
|
|
19060
19686
|
const resolved = path43.resolve(targetPath);
|
|
19061
19687
|
let realPath;
|
|
@@ -19091,6 +19717,7 @@ var init_path_guard = __esm({
|
|
|
19091
19717
|
}
|
|
19092
19718
|
return { allowed: true, reason: "" };
|
|
19093
19719
|
}
|
|
19720
|
+
/** Adds an additional allowed path at runtime (e.g. for file-service uploads). */
|
|
19094
19721
|
addAllowedPath(p2) {
|
|
19095
19722
|
try {
|
|
19096
19723
|
this.allowedPaths.push(fs38.realpathSync(path43.resolve(p2)));
|
|
@@ -19098,6 +19725,10 @@ var init_path_guard = __esm({
|
|
|
19098
19725
|
this.allowedPaths.push(path43.resolve(p2));
|
|
19099
19726
|
}
|
|
19100
19727
|
}
|
|
19728
|
+
/**
|
|
19729
|
+
* Loads additional deny patterns from .openacpignore in the workspace root.
|
|
19730
|
+
* Follows .gitignore syntax — blank lines and lines starting with # are skipped.
|
|
19731
|
+
*/
|
|
19101
19732
|
static loadIgnoreFile(cwd) {
|
|
19102
19733
|
const ignorePath = path43.join(cwd, ".openacpignore");
|
|
19103
19734
|
try {
|
|
@@ -19137,6 +19768,7 @@ var init_env_filter = __esm({
|
|
|
19137
19768
|
"src/core/security/env-filter.ts"() {
|
|
19138
19769
|
"use strict";
|
|
19139
19770
|
DEFAULT_ENV_WHITELIST = [
|
|
19771
|
+
// Shell basics — agents need these to resolve commands and write temp files
|
|
19140
19772
|
"PATH",
|
|
19141
19773
|
"HOME",
|
|
19142
19774
|
"SHELL",
|
|
@@ -19149,11 +19781,11 @@ var init_env_filter = __esm({
|
|
|
19149
19781
|
"XDG_*",
|
|
19150
19782
|
"NODE_ENV",
|
|
19151
19783
|
"EDITOR",
|
|
19152
|
-
// Git
|
|
19784
|
+
// Git — agents need git config and SSH access for code operations
|
|
19153
19785
|
"GIT_*",
|
|
19154
19786
|
"SSH_AUTH_SOCK",
|
|
19155
19787
|
"SSH_AGENT_PID",
|
|
19156
|
-
// Terminal rendering
|
|
19788
|
+
// Terminal rendering — ensures correct color output in agent responses
|
|
19157
19789
|
"COLORTERM",
|
|
19158
19790
|
"FORCE_COLOR",
|
|
19159
19791
|
"NO_COLOR",
|
|
@@ -19221,12 +19853,14 @@ var init_stderr_capture = __esm({
|
|
|
19221
19853
|
this.maxLines = maxLines;
|
|
19222
19854
|
}
|
|
19223
19855
|
lines = [];
|
|
19856
|
+
/** Append a chunk of stderr output, splitting on newlines and trimming to maxLines. */
|
|
19224
19857
|
append(chunk) {
|
|
19225
19858
|
this.lines.push(...chunk.split("\n").filter(Boolean));
|
|
19226
19859
|
if (this.lines.length > this.maxLines) {
|
|
19227
19860
|
this.lines = this.lines.slice(-this.maxLines);
|
|
19228
19861
|
}
|
|
19229
19862
|
}
|
|
19863
|
+
/** Return all captured lines joined as a single string. */
|
|
19230
19864
|
getLastLines() {
|
|
19231
19865
|
return this.lines.join("\n");
|
|
19232
19866
|
}
|
|
@@ -19245,6 +19879,7 @@ var init_typed_emitter = __esm({
|
|
|
19245
19879
|
listeners = /* @__PURE__ */ new Map();
|
|
19246
19880
|
paused = false;
|
|
19247
19881
|
buffer = [];
|
|
19882
|
+
/** Register a listener for the given event. Returns `this` for chaining. */
|
|
19248
19883
|
on(event, listener) {
|
|
19249
19884
|
let set = this.listeners.get(event);
|
|
19250
19885
|
if (!set) {
|
|
@@ -19254,10 +19889,17 @@ var init_typed_emitter = __esm({
|
|
|
19254
19889
|
set.add(listener);
|
|
19255
19890
|
return this;
|
|
19256
19891
|
}
|
|
19892
|
+
/** Remove a specific listener for the given event. */
|
|
19257
19893
|
off(event, listener) {
|
|
19258
19894
|
this.listeners.get(event)?.delete(listener);
|
|
19259
19895
|
return this;
|
|
19260
19896
|
}
|
|
19897
|
+
/**
|
|
19898
|
+
* Emit an event to all registered listeners.
|
|
19899
|
+
*
|
|
19900
|
+
* When paused, events are buffered (up to MAX_BUFFER_SIZE) unless
|
|
19901
|
+
* the passthrough filter allows them through immediately.
|
|
19902
|
+
*/
|
|
19261
19903
|
emit(event, ...args2) {
|
|
19262
19904
|
if (this.paused) {
|
|
19263
19905
|
if (this.passthroughFn?.(event, args2)) {
|
|
@@ -19301,6 +19943,7 @@ var init_typed_emitter = __esm({
|
|
|
19301
19943
|
get bufferSize() {
|
|
19302
19944
|
return this.buffer.length;
|
|
19303
19945
|
}
|
|
19946
|
+
/** Remove all listeners for a specific event, or all events if none specified. */
|
|
19304
19947
|
removeAllListeners(event) {
|
|
19305
19948
|
if (event) {
|
|
19306
19949
|
this.listeners.delete(event);
|
|
@@ -19308,6 +19951,7 @@ var init_typed_emitter = __esm({
|
|
|
19308
19951
|
this.listeners.clear();
|
|
19309
19952
|
}
|
|
19310
19953
|
}
|
|
19954
|
+
/** Deliver an event to listeners, isolating errors so one broken listener doesn't break others. */
|
|
19311
19955
|
deliver(event, args2) {
|
|
19312
19956
|
const set = this.listeners.get(event);
|
|
19313
19957
|
if (!set) return;
|
|
@@ -19379,6 +20023,11 @@ var init_terminal_manager = __esm({
|
|
|
19379
20023
|
constructor(maxOutputBytes = 1024 * 1024) {
|
|
19380
20024
|
this.maxOutputBytes = maxOutputBytes;
|
|
19381
20025
|
}
|
|
20026
|
+
/**
|
|
20027
|
+
* Spawn a new terminal process. Runs terminal:beforeCreate middleware first
|
|
20028
|
+
* (which can modify command/args/env or block creation entirely).
|
|
20029
|
+
* Returns a terminalId for subsequent output/wait/kill operations.
|
|
20030
|
+
*/
|
|
19382
20031
|
async createTerminal(sessionId, params, middlewareChain) {
|
|
19383
20032
|
let termCommand = params.command;
|
|
19384
20033
|
let termArgs = params.args ?? [];
|
|
@@ -19460,6 +20109,7 @@ var init_terminal_manager = __esm({
|
|
|
19460
20109
|
});
|
|
19461
20110
|
return { terminalId };
|
|
19462
20111
|
}
|
|
20112
|
+
/** Retrieve accumulated stdout/stderr output for a terminal. */
|
|
19463
20113
|
getOutput(terminalId) {
|
|
19464
20114
|
const state = this.terminals.get(terminalId);
|
|
19465
20115
|
if (!state) {
|
|
@@ -19474,6 +20124,7 @@ var init_terminal_manager = __esm({
|
|
|
19474
20124
|
} : void 0
|
|
19475
20125
|
};
|
|
19476
20126
|
}
|
|
20127
|
+
/** Block until the terminal process exits, returning exit code and signal. */
|
|
19477
20128
|
async waitForExit(terminalId) {
|
|
19478
20129
|
const state = this.terminals.get(terminalId);
|
|
19479
20130
|
if (!state) {
|
|
@@ -19497,6 +20148,7 @@ var init_terminal_manager = __esm({
|
|
|
19497
20148
|
}
|
|
19498
20149
|
});
|
|
19499
20150
|
}
|
|
20151
|
+
/** Send SIGTERM to a terminal process (graceful shutdown). */
|
|
19500
20152
|
kill(terminalId) {
|
|
19501
20153
|
const state = this.terminals.get(terminalId);
|
|
19502
20154
|
if (!state) {
|
|
@@ -19504,6 +20156,7 @@ var init_terminal_manager = __esm({
|
|
|
19504
20156
|
}
|
|
19505
20157
|
state.process.kill("SIGTERM");
|
|
19506
20158
|
}
|
|
20159
|
+
/** Force-kill (SIGKILL) and immediately remove a terminal from the registry. */
|
|
19507
20160
|
release(terminalId) {
|
|
19508
20161
|
const state = this.terminals.get(terminalId);
|
|
19509
20162
|
if (!state) {
|
|
@@ -19512,6 +20165,7 @@ var init_terminal_manager = __esm({
|
|
|
19512
20165
|
state.process.kill("SIGKILL");
|
|
19513
20166
|
this.terminals.delete(terminalId);
|
|
19514
20167
|
}
|
|
20168
|
+
/** Force-kill all terminals. Used during session/system teardown. */
|
|
19515
20169
|
destroyAll() {
|
|
19516
20170
|
for (const [, t] of this.terminals) {
|
|
19517
20171
|
t.process.kill("SIGKILL");
|
|
@@ -19559,6 +20213,12 @@ var init_debug_tracer = __esm({
|
|
|
19559
20213
|
}
|
|
19560
20214
|
dirCreated = false;
|
|
19561
20215
|
logDir;
|
|
20216
|
+
/**
|
|
20217
|
+
* Write a timestamped JSONL entry to the trace file for the given layer.
|
|
20218
|
+
*
|
|
20219
|
+
* Handles circular references gracefully and silently swallows errors —
|
|
20220
|
+
* debug logging must never crash the application.
|
|
20221
|
+
*/
|
|
19562
20222
|
log(layer, data) {
|
|
19563
20223
|
try {
|
|
19564
20224
|
if (!this.dirCreated) {
|
|
@@ -19716,9 +20376,13 @@ var init_agent_instance = __esm({
|
|
|
19716
20376
|
connection;
|
|
19717
20377
|
child;
|
|
19718
20378
|
stderrCapture;
|
|
20379
|
+
/** Manages terminal subprocesses that agents can spawn for shell commands. */
|
|
19719
20380
|
terminalManager = new TerminalManager();
|
|
20381
|
+
/** Shared across all instances — resolves MCP server configs for ACP sessions. */
|
|
19720
20382
|
static mcpManager = new McpManager();
|
|
20383
|
+
/** Guards against emitting crash events during intentional shutdown. */
|
|
19721
20384
|
_destroying = false;
|
|
20385
|
+
/** Restricts agent file I/O to the workspace directory and explicitly allowed paths. */
|
|
19722
20386
|
pathGuard;
|
|
19723
20387
|
sessionId;
|
|
19724
20388
|
agentName;
|
|
@@ -19728,16 +20392,36 @@ var init_agent_instance = __esm({
|
|
|
19728
20392
|
initialSessionResponse;
|
|
19729
20393
|
middlewareChain;
|
|
19730
20394
|
debugTracer = null;
|
|
19731
|
-
/**
|
|
20395
|
+
/**
|
|
20396
|
+
* Whitelist an additional filesystem path for agent read access.
|
|
20397
|
+
*
|
|
20398
|
+
* Called by SessionFactory to allow agents to read files outside the
|
|
20399
|
+
* workspace (e.g., the file-service upload directory for attachments).
|
|
20400
|
+
*/
|
|
19732
20401
|
addAllowedPath(p2) {
|
|
19733
20402
|
this.pathGuard.addAllowedPath(p2);
|
|
19734
20403
|
}
|
|
19735
|
-
// Callback — set by
|
|
20404
|
+
// Callback — set by Session/Core when wiring events. Returns the selected
|
|
20405
|
+
// permission option ID. Default no-op auto-selects the first option.
|
|
19736
20406
|
onPermissionRequest = async () => "";
|
|
19737
20407
|
constructor(agentName) {
|
|
19738
20408
|
super();
|
|
19739
20409
|
this.agentName = agentName;
|
|
19740
20410
|
}
|
|
20411
|
+
/**
|
|
20412
|
+
* Spawn the agent child process and complete the ACP protocol handshake.
|
|
20413
|
+
*
|
|
20414
|
+
* Steps:
|
|
20415
|
+
* 1. Resolve the agent command to a directly executable path
|
|
20416
|
+
* 2. Create a PathGuard scoped to the working directory
|
|
20417
|
+
* 3. Spawn the subprocess with a filtered environment (security: only whitelisted
|
|
20418
|
+
* env vars are passed to prevent leaking secrets like API keys)
|
|
20419
|
+
* 4. Wire stdin/stdout through debug-tracing Transform streams
|
|
20420
|
+
* 5. Convert Node streams → Web streams for the ACP SDK
|
|
20421
|
+
* 6. Perform the ACP `initialize` handshake and negotiate capabilities
|
|
20422
|
+
*
|
|
20423
|
+
* Does NOT create a session — callers must follow up with newSession or loadSession.
|
|
20424
|
+
*/
|
|
19741
20425
|
static async spawnSubprocess(agentDef, workingDirectory, allowedPaths = []) {
|
|
19742
20426
|
const instance = new _AgentInstance(agentDef.name);
|
|
19743
20427
|
const resolved = resolveAgentCommand(agentDef.command);
|
|
@@ -19836,6 +20520,13 @@ var init_agent_instance = __esm({
|
|
|
19836
20520
|
);
|
|
19837
20521
|
return instance;
|
|
19838
20522
|
}
|
|
20523
|
+
/**
|
|
20524
|
+
* Monitor the subprocess for unexpected exits and emit error events.
|
|
20525
|
+
*
|
|
20526
|
+
* Distinguishes intentional shutdown (SIGTERM/SIGINT during destroy) from
|
|
20527
|
+
* crashes (non-zero exit code or unexpected signal). Crash events include
|
|
20528
|
+
* captured stderr output for diagnostic context.
|
|
20529
|
+
*/
|
|
19839
20530
|
setupCrashDetection() {
|
|
19840
20531
|
this.child.on("exit", (code, signal) => {
|
|
19841
20532
|
if (this._destroying) return;
|
|
@@ -19858,6 +20549,18 @@ ${stderr}`
|
|
|
19858
20549
|
log33.debug({ sessionId: this.sessionId }, "ACP connection closed");
|
|
19859
20550
|
});
|
|
19860
20551
|
}
|
|
20552
|
+
/**
|
|
20553
|
+
* Spawn a new agent subprocess and create a fresh ACP session.
|
|
20554
|
+
*
|
|
20555
|
+
* This is the primary entry point for starting an agent. It spawns the
|
|
20556
|
+
* subprocess, completes the ACP handshake, and calls `newSession` to
|
|
20557
|
+
* initialize the agent's working context (cwd, MCP servers).
|
|
20558
|
+
*
|
|
20559
|
+
* @param agentDef - Agent definition (command, args, env) from the catalog
|
|
20560
|
+
* @param workingDirectory - Workspace root the agent operates in
|
|
20561
|
+
* @param mcpServers - Optional MCP server configs to extend agent capabilities
|
|
20562
|
+
* @param allowedPaths - Extra filesystem paths the agent may access
|
|
20563
|
+
*/
|
|
19861
20564
|
static async spawn(agentDef, workingDirectory, mcpServers, allowedPaths) {
|
|
19862
20565
|
log33.debug(
|
|
19863
20566
|
{ agentName: agentDef.name, command: agentDef.command },
|
|
@@ -19891,6 +20594,15 @@ ${stderr}`
|
|
|
19891
20594
|
);
|
|
19892
20595
|
return instance;
|
|
19893
20596
|
}
|
|
20597
|
+
/**
|
|
20598
|
+
* Spawn a new subprocess and restore an existing agent session.
|
|
20599
|
+
*
|
|
20600
|
+
* Tries loadSession first (preferred, stable API), falls back to the
|
|
20601
|
+
* unstable resumeSession, and finally falls back to creating a brand-new
|
|
20602
|
+
* session if resume fails entirely (e.g., agent lost its state).
|
|
20603
|
+
*
|
|
20604
|
+
* @param agentSessionId - The agent-side session ID to restore
|
|
20605
|
+
*/
|
|
19894
20606
|
static async resume(agentDef, workingDirectory, agentSessionId, mcpServers, allowedPaths) {
|
|
19895
20607
|
log33.debug({ agentName: agentDef.name, agentSessionId }, "Resuming agent");
|
|
19896
20608
|
const spawnStart = Date.now();
|
|
@@ -19957,12 +20669,26 @@ ${stderr}`
|
|
|
19957
20669
|
instance.setupCrashDetection();
|
|
19958
20670
|
return instance;
|
|
19959
20671
|
}
|
|
19960
|
-
|
|
20672
|
+
/**
|
|
20673
|
+
* Build the ACP Client callback object.
|
|
20674
|
+
*
|
|
20675
|
+
* The ACP SDK invokes these callbacks when the agent sends notifications
|
|
20676
|
+
* or requests. Each callback maps an ACP protocol message to either:
|
|
20677
|
+
* - An internal AgentEvent (emitted for Session/adapters to consume)
|
|
20678
|
+
* - A filesystem or terminal operation (executed on the agent's behalf)
|
|
20679
|
+
* - A permission request (proxied to the user via the adapter)
|
|
20680
|
+
*/
|
|
19961
20681
|
createClient(_agent) {
|
|
19962
20682
|
const self = this;
|
|
19963
20683
|
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
19964
20684
|
return {
|
|
19965
20685
|
// ── Session updates ──────────────────────────────────────────────────
|
|
20686
|
+
// The agent streams its response as a series of session update events.
|
|
20687
|
+
// Each event type maps to an internal AgentEvent that Session relays
|
|
20688
|
+
// to adapters for rendering (text chunks, tool calls, usage stats, etc.).
|
|
20689
|
+
// Chunks are forwarded to Session individually as they arrive — no buffering
|
|
20690
|
+
// happens at this layer. If buffering is needed (e.g., to avoid rate limits),
|
|
20691
|
+
// it is the responsibility of the Session or adapter layer.
|
|
19966
20692
|
async sessionUpdate(params) {
|
|
19967
20693
|
const update = params.update;
|
|
19968
20694
|
let event = null;
|
|
@@ -20091,6 +20817,10 @@ ${stderr}`
|
|
|
20091
20817
|
}
|
|
20092
20818
|
},
|
|
20093
20819
|
// ── Permission requests ──────────────────────────────────────────────
|
|
20820
|
+
// The agent needs user approval before performing sensitive operations
|
|
20821
|
+
// (e.g., file writes, shell commands). This proxies the request up
|
|
20822
|
+
// through Session → PermissionGate → adapter → user, then returns
|
|
20823
|
+
// the user's chosen option ID back to the agent.
|
|
20094
20824
|
async requestPermission(params) {
|
|
20095
20825
|
const permissionRequest = {
|
|
20096
20826
|
id: params.toolCall.toolCallId,
|
|
@@ -20107,6 +20837,9 @@ ${stderr}`
|
|
|
20107
20837
|
};
|
|
20108
20838
|
},
|
|
20109
20839
|
// ── File operations ──────────────────────────────────────────────────
|
|
20840
|
+
// The agent reads/writes files through these callbacks rather than
|
|
20841
|
+
// accessing the filesystem directly. This allows PathGuard to enforce
|
|
20842
|
+
// workspace boundaries and middleware hooks to intercept I/O.
|
|
20110
20843
|
async readTextFile(params) {
|
|
20111
20844
|
const p2 = params;
|
|
20112
20845
|
const pathCheck = self.pathGuard.validatePath(p2.path, "read");
|
|
@@ -20142,6 +20875,8 @@ ${stderr}`
|
|
|
20142
20875
|
return {};
|
|
20143
20876
|
},
|
|
20144
20877
|
// ── Terminal operations (delegated to TerminalManager) ─────────────
|
|
20878
|
+
// Agents can spawn shell commands via terminal operations. TerminalManager
|
|
20879
|
+
// handles subprocess lifecycle, output capture, and byte-limit enforcement.
|
|
20145
20880
|
async createTerminal(params) {
|
|
20146
20881
|
return self.terminalManager.createTerminal(
|
|
20147
20882
|
self.sessionId,
|
|
@@ -20171,6 +20906,13 @@ ${stderr}`
|
|
|
20171
20906
|
};
|
|
20172
20907
|
}
|
|
20173
20908
|
// ── New ACP methods ──────────────────────────────────────────────────
|
|
20909
|
+
/**
|
|
20910
|
+
* Update a session config option (mode, model, etc.) on the agent.
|
|
20911
|
+
*
|
|
20912
|
+
* Falls back to legacy `setSessionMode`/`unstable_setSessionModel` methods
|
|
20913
|
+
* for agents that haven't adopted the unified `session/set_config_option`
|
|
20914
|
+
* ACP method (detected via JSON-RPC -32601 "Method Not Found" error).
|
|
20915
|
+
*/
|
|
20174
20916
|
async setConfigOption(configId, value) {
|
|
20175
20917
|
try {
|
|
20176
20918
|
return await this.connection.setSessionConfigOption({
|
|
@@ -20198,12 +20940,14 @@ ${stderr}`
|
|
|
20198
20940
|
throw err;
|
|
20199
20941
|
}
|
|
20200
20942
|
}
|
|
20943
|
+
/** List the agent's known sessions, optionally filtered by working directory. */
|
|
20201
20944
|
async listSessions(cwd, cursor) {
|
|
20202
20945
|
return await this.connection.listSessions({
|
|
20203
20946
|
cwd: cwd ?? null,
|
|
20204
20947
|
cursor: cursor ?? null
|
|
20205
20948
|
});
|
|
20206
20949
|
}
|
|
20950
|
+
/** Load an existing agent session by ID into this subprocess. */
|
|
20207
20951
|
async loadSession(sessionId, cwd, mcpServers) {
|
|
20208
20952
|
const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
|
|
20209
20953
|
return await this.connection.loadSession({
|
|
@@ -20212,9 +20956,11 @@ ${stderr}`
|
|
|
20212
20956
|
mcpServers: resolvedMcp
|
|
20213
20957
|
});
|
|
20214
20958
|
}
|
|
20959
|
+
/** Trigger agent-managed authentication (e.g., OAuth flow). */
|
|
20215
20960
|
async authenticate(methodId) {
|
|
20216
20961
|
await this.connection.authenticate({ methodId });
|
|
20217
20962
|
}
|
|
20963
|
+
/** Fork an existing session, creating a new branch with shared history. */
|
|
20218
20964
|
async forkSession(sessionId, cwd, mcpServers) {
|
|
20219
20965
|
const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
|
|
20220
20966
|
return await this.connection.unstable_forkSession({
|
|
@@ -20223,10 +20969,25 @@ ${stderr}`
|
|
|
20223
20969
|
mcpServers: resolvedMcp
|
|
20224
20970
|
});
|
|
20225
20971
|
}
|
|
20972
|
+
/** Close a session on the agent side (cleanup agent-internal state). */
|
|
20226
20973
|
async closeSession(sessionId) {
|
|
20227
20974
|
await this.connection.unstable_closeSession({ sessionId });
|
|
20228
20975
|
}
|
|
20229
20976
|
// ── Prompt & lifecycle ──────────────────────────────────────────────
|
|
20977
|
+
/**
|
|
20978
|
+
* Send a user prompt to the agent and wait for the complete response.
|
|
20979
|
+
*
|
|
20980
|
+
* Builds ACP content blocks from the text and any attachments (images
|
|
20981
|
+
* are base64-encoded if the agent supports them, otherwise appended as
|
|
20982
|
+
* file paths). The promise resolves when the agent finishes responding;
|
|
20983
|
+
* streaming events arrive via the `agent_event` emitter during execution.
|
|
20984
|
+
*
|
|
20985
|
+
* Attachments that exceed size limits or use unsupported formats are
|
|
20986
|
+
* skipped with a note appended to the prompt text.
|
|
20987
|
+
*
|
|
20988
|
+
* Call `cancel()` to interrupt a running prompt; the agent will stop and
|
|
20989
|
+
* the promise resolves with partial results.
|
|
20990
|
+
*/
|
|
20230
20991
|
async prompt(text5, attachments) {
|
|
20231
20992
|
const contentBlocks = [{ type: "text", text: text5 }];
|
|
20232
20993
|
const capabilities = this.promptCapabilities ?? {};
|
|
@@ -20276,9 +21037,18 @@ ${skipNote}`;
|
|
|
20276
21037
|
prompt: contentBlocks
|
|
20277
21038
|
});
|
|
20278
21039
|
}
|
|
21040
|
+
/** Cancel the currently running prompt. The agent should stop and return partial results. */
|
|
20279
21041
|
async cancel() {
|
|
20280
21042
|
await this.connection.cancel({ sessionId: this.sessionId });
|
|
20281
21043
|
}
|
|
21044
|
+
/**
|
|
21045
|
+
* Gracefully shut down the agent subprocess.
|
|
21046
|
+
*
|
|
21047
|
+
* Sends SIGTERM first, giving the agent up to 10 seconds to clean up.
|
|
21048
|
+
* If the process hasn't exited by then, SIGKILL forces termination.
|
|
21049
|
+
* The timer is unref'd so it doesn't keep the Node process alive
|
|
21050
|
+
* during shutdown.
|
|
21051
|
+
*/
|
|
20282
21052
|
async destroy() {
|
|
20283
21053
|
this._destroying = true;
|
|
20284
21054
|
this.terminalManager.destroyAll();
|
|
@@ -20313,6 +21083,7 @@ var init_agent_manager = __esm({
|
|
|
20313
21083
|
constructor(catalog) {
|
|
20314
21084
|
this.catalog = catalog;
|
|
20315
21085
|
}
|
|
21086
|
+
/** Return definitions for all installed agents. */
|
|
20316
21087
|
getAvailableAgents() {
|
|
20317
21088
|
const installed = this.catalog.getInstalledEntries();
|
|
20318
21089
|
return Object.entries(installed).map(([key, agent]) => ({
|
|
@@ -20322,14 +21093,25 @@ var init_agent_manager = __esm({
|
|
|
20322
21093
|
env: agent.env
|
|
20323
21094
|
}));
|
|
20324
21095
|
}
|
|
21096
|
+
/** Look up a single agent definition by its short name (e.g., "claude", "gemini"). */
|
|
20325
21097
|
getAgent(name) {
|
|
20326
21098
|
return this.catalog.resolve(name);
|
|
20327
21099
|
}
|
|
21100
|
+
/**
|
|
21101
|
+
* Spawn a new agent subprocess with a fresh session.
|
|
21102
|
+
*
|
|
21103
|
+
* @throws If the agent is not installed — includes install instructions in the error message.
|
|
21104
|
+
*/
|
|
20328
21105
|
async spawn(agentName, workingDirectory, allowedPaths) {
|
|
20329
21106
|
const agentDef = this.getAgent(agentName);
|
|
20330
21107
|
if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
|
|
20331
21108
|
return AgentInstance.spawn(agentDef, workingDirectory, void 0, allowedPaths);
|
|
20332
21109
|
}
|
|
21110
|
+
/**
|
|
21111
|
+
* Spawn a subprocess and resume an existing agent session.
|
|
21112
|
+
*
|
|
21113
|
+
* Falls back to a new session if the agent cannot restore the given session ID.
|
|
21114
|
+
*/
|
|
20333
21115
|
async resume(agentName, workingDirectory, agentSessionId, allowedPaths) {
|
|
20334
21116
|
const agentDef = this.getAgent(agentName);
|
|
20335
21117
|
if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
|
|
@@ -20354,6 +21136,11 @@ var init_prompt_queue = __esm({
|
|
|
20354
21136
|
abortController = null;
|
|
20355
21137
|
/** Set when abort is triggered; drainNext waits for the current processor to settle before starting the next item. */
|
|
20356
21138
|
processorSettled = null;
|
|
21139
|
+
/**
|
|
21140
|
+
* Add a prompt to the queue. If no prompt is currently processing, it runs
|
|
21141
|
+
* immediately. Otherwise, it's buffered and the returned promise resolves
|
|
21142
|
+
* only after the prompt finishes processing.
|
|
21143
|
+
*/
|
|
20357
21144
|
async enqueue(text5, attachments, routing, turnId) {
|
|
20358
21145
|
if (this.processing) {
|
|
20359
21146
|
return new Promise((resolve9) => {
|
|
@@ -20362,6 +21149,7 @@ var init_prompt_queue = __esm({
|
|
|
20362
21149
|
}
|
|
20363
21150
|
await this.process(text5, attachments, routing, turnId);
|
|
20364
21151
|
}
|
|
21152
|
+
/** Run a single prompt through the processor, then drain the next queued item. */
|
|
20365
21153
|
async process(text5, attachments, routing, turnId) {
|
|
20366
21154
|
this.processing = true;
|
|
20367
21155
|
this.abortController = new AbortController();
|
|
@@ -20389,12 +21177,17 @@ var init_prompt_queue = __esm({
|
|
|
20389
21177
|
this.drainNext();
|
|
20390
21178
|
}
|
|
20391
21179
|
}
|
|
21180
|
+
/** Dequeue and process the next pending prompt, if any. Called after each prompt completes. */
|
|
20392
21181
|
drainNext() {
|
|
20393
21182
|
const next = this.queue.shift();
|
|
20394
21183
|
if (next) {
|
|
20395
21184
|
this.process(next.text, next.attachments, next.routing, next.turnId).then(next.resolve);
|
|
20396
21185
|
}
|
|
20397
21186
|
}
|
|
21187
|
+
/**
|
|
21188
|
+
* Abort the in-flight prompt and discard all queued prompts.
|
|
21189
|
+
* Pending promises are resolved (not rejected) so callers don't see unhandled rejections.
|
|
21190
|
+
*/
|
|
20398
21191
|
clear() {
|
|
20399
21192
|
if (this.abortController) {
|
|
20400
21193
|
this.abortController.abort();
|
|
@@ -20430,6 +21223,10 @@ var init_permission_gate = __esm({
|
|
|
20430
21223
|
constructor(timeoutMs) {
|
|
20431
21224
|
this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
20432
21225
|
}
|
|
21226
|
+
/**
|
|
21227
|
+
* Register a new permission request and return a promise that resolves with the
|
|
21228
|
+
* chosen option ID when the user responds, or rejects on timeout / supersession.
|
|
21229
|
+
*/
|
|
20433
21230
|
setPending(request) {
|
|
20434
21231
|
if (!this.settled && this.rejectFn) {
|
|
20435
21232
|
this.rejectFn(new Error("Superseded by new permission request"));
|
|
@@ -20448,6 +21245,7 @@ var init_permission_gate = __esm({
|
|
|
20448
21245
|
}
|
|
20449
21246
|
});
|
|
20450
21247
|
}
|
|
21248
|
+
/** Approve the pending request with the given option ID. No-op if already settled. */
|
|
20451
21249
|
resolve(optionId) {
|
|
20452
21250
|
if (this.settled || !this.resolveFn) return;
|
|
20453
21251
|
this.settled = true;
|
|
@@ -20455,6 +21253,7 @@ var init_permission_gate = __esm({
|
|
|
20455
21253
|
this.resolveFn(optionId);
|
|
20456
21254
|
this.cleanup();
|
|
20457
21255
|
}
|
|
21256
|
+
/** Deny the pending request. No-op if already settled. */
|
|
20458
21257
|
reject(reason) {
|
|
20459
21258
|
if (this.settled || !this.rejectFn) return;
|
|
20460
21259
|
this.settled = true;
|
|
@@ -20563,6 +21362,8 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
|
|
|
20563
21362
|
get agentInstance() {
|
|
20564
21363
|
return this._agentInstance;
|
|
20565
21364
|
}
|
|
21365
|
+
/** Setting agentInstance wires the agent→session event relay and commands buffer.
|
|
21366
|
+
* This happens both at construction and on agent switch (switchAgent). */
|
|
20566
21367
|
set agentInstance(agent) {
|
|
20567
21368
|
this._agentInstance = agent;
|
|
20568
21369
|
this.wireAgentRelay();
|
|
@@ -20665,7 +21466,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
|
|
|
20665
21466
|
this.transition("finished");
|
|
20666
21467
|
this.emit(SessionEv.SESSION_END, reason ?? "completed");
|
|
20667
21468
|
}
|
|
20668
|
-
/** Transition to cancelled — from active
|
|
21469
|
+
/** Transition to cancelled — from active or error (terminal session cancel) */
|
|
20669
21470
|
markCancelled() {
|
|
20670
21471
|
this.transition("cancelled");
|
|
20671
21472
|
}
|
|
@@ -20685,19 +21486,29 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
|
|
|
20685
21486
|
get queueDepth() {
|
|
20686
21487
|
return this.queue.pending;
|
|
20687
21488
|
}
|
|
21489
|
+
/** Whether a prompt is currently being processed by the agent */
|
|
20688
21490
|
get promptRunning() {
|
|
20689
21491
|
return this.queue.isProcessing;
|
|
20690
21492
|
}
|
|
20691
21493
|
// --- Context Injection ---
|
|
21494
|
+
/** Store context markdown to be prepended to the next prompt (used for session resume with history). */
|
|
20692
21495
|
setContext(markdown) {
|
|
20693
21496
|
this.pendingContext = markdown;
|
|
20694
21497
|
}
|
|
20695
21498
|
// --- Voice Mode ---
|
|
21499
|
+
/** Set TTS mode: "off" = disabled, "next" = one-shot (auto-resets after prompt), "on" = persistent. */
|
|
20696
21500
|
setVoiceMode(mode) {
|
|
20697
21501
|
this.voiceMode = mode;
|
|
20698
21502
|
this.log.info({ voiceMode: mode }, "TTS mode changed");
|
|
20699
21503
|
}
|
|
20700
21504
|
// --- Public API ---
|
|
21505
|
+
/**
|
|
21506
|
+
* Enqueue a user prompt for serial processing.
|
|
21507
|
+
*
|
|
21508
|
+
* Runs the prompt through agent:beforePrompt middleware (which can modify or block),
|
|
21509
|
+
* then adds it to the PromptQueue. Returns a turnId that callers can use to correlate
|
|
21510
|
+
* queued/processing events before the prompt actually runs.
|
|
21511
|
+
*/
|
|
20701
21512
|
async enqueuePrompt(text5, attachments, routing, externalTurnId) {
|
|
20702
21513
|
const turnId = externalTurnId ?? nanoid5(8);
|
|
20703
21514
|
if (this.middlewareChain) {
|
|
@@ -20807,6 +21618,10 @@ ${text5}`;
|
|
|
20807
21618
|
await this.autoName();
|
|
20808
21619
|
}
|
|
20809
21620
|
}
|
|
21621
|
+
/**
|
|
21622
|
+
* Transcribe audio attachments to text if the agent doesn't support audio natively.
|
|
21623
|
+
* Audio attachments are removed and their transcriptions are appended to the prompt text.
|
|
21624
|
+
*/
|
|
20810
21625
|
async maybeTranscribeAudio(text5, attachments) {
|
|
20811
21626
|
if (!attachments?.length || !this.speechService) {
|
|
20812
21627
|
return { text: text5, attachments };
|
|
@@ -20852,6 +21667,7 @@ ${result.text}` : result.text;
|
|
|
20852
21667
|
attachments: remainingAttachments.length > 0 ? remainingAttachments : void 0
|
|
20853
21668
|
};
|
|
20854
21669
|
}
|
|
21670
|
+
/** Extract [TTS] block from agent response, synthesize speech, and emit audio_content event. */
|
|
20855
21671
|
async processTTSResponse(responseText) {
|
|
20856
21672
|
const match = TTS_BLOCK_REGEX.exec(responseText);
|
|
20857
21673
|
if (!match?.[1]) {
|
|
@@ -20888,7 +21704,9 @@ ${result.text}` : result.text;
|
|
|
20888
21704
|
this.log.warn({ err }, "TTS synthesis failed, skipping");
|
|
20889
21705
|
}
|
|
20890
21706
|
}
|
|
20891
|
-
//
|
|
21707
|
+
// Sends a special prompt to the agent to generate a short session title.
|
|
21708
|
+
// The session emitter is paused (excluding non-agent_event emissions) so the naming
|
|
21709
|
+
// prompt's output is intercepted by a capture handler instead of being forwarded to adapters.
|
|
20892
21710
|
async autoName() {
|
|
20893
21711
|
let title = "";
|
|
20894
21712
|
const captureHandler = (event) => {
|
|
@@ -21047,6 +21865,7 @@ ${result.text}` : result.text;
|
|
|
21047
21865
|
this.applySpawnResponse(newAgent.initialSessionResponse, newAgent.agentCapabilities);
|
|
21048
21866
|
this.log.info({ from: this.agentSwitchHistory.at(-1).agentName, to: agentName }, "Agent switched");
|
|
21049
21867
|
}
|
|
21868
|
+
/** Tear down the session: reject pending permissions, clear queue, destroy agent subprocess. */
|
|
21050
21869
|
async destroy() {
|
|
21051
21870
|
this.log.info("Session destroyed");
|
|
21052
21871
|
if (this.permissionGate.isPending) {
|
|
@@ -21072,12 +21891,17 @@ var init_session_manager = __esm({
|
|
|
21072
21891
|
store;
|
|
21073
21892
|
eventBus;
|
|
21074
21893
|
middlewareChain;
|
|
21894
|
+
/**
|
|
21895
|
+
* Inject the EventBus after construction. Deferred because EventBus is created
|
|
21896
|
+
* after SessionManager during bootstrap, so it cannot be passed to the constructor.
|
|
21897
|
+
*/
|
|
21075
21898
|
setEventBus(eventBus) {
|
|
21076
21899
|
this.eventBus = eventBus;
|
|
21077
21900
|
}
|
|
21078
21901
|
constructor(store = null) {
|
|
21079
21902
|
this.store = store;
|
|
21080
21903
|
}
|
|
21904
|
+
/** Create a new session by spawning an agent and persisting the initial record. */
|
|
21081
21905
|
async createSession(channelId, agentName, workingDirectory, agentManager) {
|
|
21082
21906
|
const agentInstance = await agentManager.spawn(agentName, workingDirectory);
|
|
21083
21907
|
const session = new Session({
|
|
@@ -21105,9 +21929,11 @@ var init_session_manager = __esm({
|
|
|
21105
21929
|
}
|
|
21106
21930
|
return session;
|
|
21107
21931
|
}
|
|
21932
|
+
/** Look up a live session by its OpenACP session ID. */
|
|
21108
21933
|
getSession(sessionId) {
|
|
21109
21934
|
return this.sessions.get(sessionId);
|
|
21110
21935
|
}
|
|
21936
|
+
/** Look up a live session by adapter channel and thread ID (checks per-adapter threadIds map first, then legacy fields). */
|
|
21111
21937
|
getSessionByThread(channelId, threadId) {
|
|
21112
21938
|
for (const session of this.sessions.values()) {
|
|
21113
21939
|
const adapterThread = session.threadIds.get(channelId);
|
|
@@ -21118,6 +21944,7 @@ var init_session_manager = __esm({
|
|
|
21118
21944
|
}
|
|
21119
21945
|
return void 0;
|
|
21120
21946
|
}
|
|
21947
|
+
/** Look up a live session by the agent's internal session ID (assigned by the ACP subprocess). */
|
|
21121
21948
|
getSessionByAgentSessionId(agentSessionId) {
|
|
21122
21949
|
for (const session of this.sessions.values()) {
|
|
21123
21950
|
if (session.agentSessionId === agentSessionId) {
|
|
@@ -21126,18 +21953,26 @@ var init_session_manager = __esm({
|
|
|
21126
21953
|
}
|
|
21127
21954
|
return void 0;
|
|
21128
21955
|
}
|
|
21956
|
+
/** Look up the persisted SessionRecord by the agent's internal session ID. */
|
|
21129
21957
|
getRecordByAgentSessionId(agentSessionId) {
|
|
21130
21958
|
return this.store?.findByAgentSessionId(agentSessionId);
|
|
21131
21959
|
}
|
|
21960
|
+
/** Look up the persisted SessionRecord by channel and thread ID. */
|
|
21132
21961
|
getRecordByThread(channelId, threadId) {
|
|
21133
21962
|
return this.store?.findByPlatform(
|
|
21134
21963
|
channelId,
|
|
21135
21964
|
(p2) => String(p2.topicId) === threadId || p2.threadId === threadId
|
|
21136
21965
|
);
|
|
21137
21966
|
}
|
|
21967
|
+
/** Register a session that was created externally (e.g. restored from store on startup). */
|
|
21138
21968
|
registerSession(session) {
|
|
21139
21969
|
this.sessions.set(session.id, session);
|
|
21140
21970
|
}
|
|
21971
|
+
/**
|
|
21972
|
+
* Merge a partial update into the stored SessionRecord. If no record exists yet and
|
|
21973
|
+
* the patch includes `sessionId`, it is treated as an initial save.
|
|
21974
|
+
* Pass `{ immediate: true }` to flush the store to disk synchronously.
|
|
21975
|
+
*/
|
|
21141
21976
|
async patchRecord(sessionId, patch, options) {
|
|
21142
21977
|
if (!this.store) return;
|
|
21143
21978
|
const record = this.store.get(sessionId);
|
|
@@ -21150,9 +21985,11 @@ var init_session_manager = __esm({
|
|
|
21150
21985
|
this.store.flush();
|
|
21151
21986
|
}
|
|
21152
21987
|
}
|
|
21988
|
+
/** Retrieve the persisted SessionRecord for a given session ID. Returns undefined if no store or record not found. */
|
|
21153
21989
|
getSessionRecord(sessionId) {
|
|
21154
21990
|
return this.store?.get(sessionId);
|
|
21155
21991
|
}
|
|
21992
|
+
/** Cancel a session: abort in-flight prompt, transition to cancelled, destroy agent, and persist. */
|
|
21156
21993
|
async cancelSession(sessionId) {
|
|
21157
21994
|
const session = this.sessions.get(sessionId);
|
|
21158
21995
|
if (session) {
|
|
@@ -21175,11 +22012,16 @@ var init_session_manager = __esm({
|
|
|
21175
22012
|
});
|
|
21176
22013
|
}
|
|
21177
22014
|
}
|
|
22015
|
+
/** List live (in-memory) sessions, optionally filtered by channel. Excludes assistant sessions. */
|
|
21178
22016
|
listSessions(channelId) {
|
|
21179
22017
|
const all = Array.from(this.sessions.values()).filter((s) => !s.isAssistant);
|
|
21180
22018
|
if (channelId) return all.filter((s) => s.channelId === channelId);
|
|
21181
22019
|
return all;
|
|
21182
22020
|
}
|
|
22021
|
+
/**
|
|
22022
|
+
* List all sessions (live + stored) as SessionSummary. Live sessions take precedence
|
|
22023
|
+
* over stored records — their real-time state (queueDepth, promptRunning) is used.
|
|
22024
|
+
*/
|
|
21183
22025
|
listAllSessions(channelId) {
|
|
21184
22026
|
if (this.store) {
|
|
21185
22027
|
let records = this.store.list().filter((r) => !r.isAssistant);
|
|
@@ -21241,6 +22083,7 @@ var init_session_manager = __esm({
|
|
|
21241
22083
|
isLive: true
|
|
21242
22084
|
}));
|
|
21243
22085
|
}
|
|
22086
|
+
/** List all stored SessionRecords, optionally filtered by status. Excludes assistant sessions. */
|
|
21244
22087
|
listRecords(filter) {
|
|
21245
22088
|
if (!this.store) return [];
|
|
21246
22089
|
let records = this.store.list().filter((r) => !r.isAssistant);
|
|
@@ -21249,6 +22092,7 @@ var init_session_manager = __esm({
|
|
|
21249
22092
|
}
|
|
21250
22093
|
return records;
|
|
21251
22094
|
}
|
|
22095
|
+
/** Remove a session's stored record and emit a SESSION_DELETED event. */
|
|
21252
22096
|
async removeRecord(sessionId) {
|
|
21253
22097
|
if (!this.store) return;
|
|
21254
22098
|
await this.store.remove(sessionId);
|
|
@@ -21359,7 +22203,10 @@ var init_session_bridge = __esm({
|
|
|
21359
22203
|
log34.error({ err, sessionId }, "Error in sendMessage middleware");
|
|
21360
22204
|
}
|
|
21361
22205
|
}
|
|
21362
|
-
/**
|
|
22206
|
+
/**
|
|
22207
|
+
* Determine if this bridge should forward the given event based on turn routing.
|
|
22208
|
+
* System events are always forwarded; turn events are routed only to the target adapter.
|
|
22209
|
+
*/
|
|
21363
22210
|
shouldForward(event) {
|
|
21364
22211
|
if (isSystemEvent(event)) return true;
|
|
21365
22212
|
const ctx = this.session.activeTurnContext;
|
|
@@ -21368,6 +22215,13 @@ var init_session_bridge = __esm({
|
|
|
21368
22215
|
if (target === null) return false;
|
|
21369
22216
|
return this.adapterId === target;
|
|
21370
22217
|
}
|
|
22218
|
+
/**
|
|
22219
|
+
* Subscribe to session events and start forwarding them to the adapter.
|
|
22220
|
+
*
|
|
22221
|
+
* Wires: agent events → adapter dispatch, permission UI, lifecycle persistence
|
|
22222
|
+
* (status changes, naming, prompt count), and EventBus notifications.
|
|
22223
|
+
* Also replays any commands or config options that arrived before the bridge connected.
|
|
22224
|
+
*/
|
|
21371
22225
|
connect() {
|
|
21372
22226
|
if (this.connected) return;
|
|
21373
22227
|
this.connected = true;
|
|
@@ -21444,6 +22298,7 @@ var init_session_bridge = __esm({
|
|
|
21444
22298
|
this.session.emit(SessionEv.AGENT_EVENT, { type: "config_option_update", options: this.session.configOptions });
|
|
21445
22299
|
}
|
|
21446
22300
|
}
|
|
22301
|
+
/** Unsubscribe all session event listeners and clean up adapter state. */
|
|
21447
22302
|
disconnect() {
|
|
21448
22303
|
if (!this.connected) return;
|
|
21449
22304
|
this.connected = false;
|
|
@@ -21976,6 +22831,12 @@ var init_message_transformer = __esm({
|
|
|
21976
22831
|
constructor(tunnelService) {
|
|
21977
22832
|
this.tunnelService = tunnelService;
|
|
21978
22833
|
}
|
|
22834
|
+
/**
|
|
22835
|
+
* Convert an agent event to an outgoing message for adapter delivery.
|
|
22836
|
+
*
|
|
22837
|
+
* For tool events, enriches the metadata with diff stats and viewer links
|
|
22838
|
+
* when a tunnel service is available.
|
|
22839
|
+
*/
|
|
21979
22840
|
transform(event, sessionContext) {
|
|
21980
22841
|
switch (event.type) {
|
|
21981
22842
|
case "text":
|
|
@@ -22250,6 +23111,11 @@ var init_session_store = __esm({
|
|
|
22250
23111
|
}
|
|
22251
23112
|
return void 0;
|
|
22252
23113
|
}
|
|
23114
|
+
/**
|
|
23115
|
+
* Find a session by its ACP agent session ID.
|
|
23116
|
+
* Checks current, original, and historical agent session IDs (from agent switches)
|
|
23117
|
+
* since the agent session ID changes on each switch.
|
|
23118
|
+
*/
|
|
22253
23119
|
findByAgentSessionId(agentSessionId) {
|
|
22254
23120
|
for (const record of this.records.values()) {
|
|
22255
23121
|
if (record.agentSessionId === agentSessionId || record.originalAgentSessionId === agentSessionId) {
|
|
@@ -22294,6 +23160,7 @@ var init_session_store = __esm({
|
|
|
22294
23160
|
if (!fs42.existsSync(dir)) fs42.mkdirSync(dir, { recursive: true });
|
|
22295
23161
|
fs42.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
22296
23162
|
}
|
|
23163
|
+
/** Clean up timers and process listeners. Call on shutdown to prevent leaks. */
|
|
22297
23164
|
destroy() {
|
|
22298
23165
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
22299
23166
|
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
|
|
@@ -22329,7 +23196,11 @@ var init_session_store = __esm({
|
|
|
22329
23196
|
}
|
|
22330
23197
|
}
|
|
22331
23198
|
}
|
|
22332
|
-
/**
|
|
23199
|
+
/**
|
|
23200
|
+
* Migrate old SessionRecord format to new multi-adapter format.
|
|
23201
|
+
* Converts single-adapter `platform` field to per-adapter `platforms` map,
|
|
23202
|
+
* and initializes `attachedAdapters` for records created before multi-adapter support.
|
|
23203
|
+
*/
|
|
22333
23204
|
migrateRecord(record) {
|
|
22334
23205
|
if (!record.platforms && record.platform && typeof record.platform === "object") {
|
|
22335
23206
|
const platformData = record.platform;
|
|
@@ -22342,6 +23213,7 @@ var init_session_store = __esm({
|
|
|
22342
23213
|
}
|
|
22343
23214
|
return record;
|
|
22344
23215
|
}
|
|
23216
|
+
/** Remove expired session records (past TTL). Active and assistant sessions are preserved. */
|
|
22345
23217
|
cleanup() {
|
|
22346
23218
|
const cutoff = Date.now() - this.ttlDays * 24 * 60 * 60 * 1e3;
|
|
22347
23219
|
let removed = 0;
|
|
@@ -22412,6 +23284,10 @@ var init_session_factory = __esm({
|
|
|
22412
23284
|
get speechService() {
|
|
22413
23285
|
return typeof this.speechServiceAccessor === "function" ? this.speechServiceAccessor() : this.speechServiceAccessor;
|
|
22414
23286
|
}
|
|
23287
|
+
/**
|
|
23288
|
+
* Create a new Session: spawn agent → create Session instance → hydrate ACP state → register.
|
|
23289
|
+
* Runs session:beforeCreate middleware (which can modify params or block creation).
|
|
23290
|
+
*/
|
|
22415
23291
|
async create(params) {
|
|
22416
23292
|
let createParams = params;
|
|
22417
23293
|
if (this.middlewareChain) {
|
|
@@ -22595,6 +23471,11 @@ var init_session_factory = __esm({
|
|
|
22595
23471
|
this.resumeLocks.set(sessionId, resumePromise);
|
|
22596
23472
|
return resumePromise;
|
|
22597
23473
|
}
|
|
23474
|
+
/**
|
|
23475
|
+
* Attempt to resume a session from disk when a message arrives on a thread with
|
|
23476
|
+
* no live session. Deduplicates concurrent resume attempts for the same thread
|
|
23477
|
+
* via resumeLocks to avoid spawning multiple agents.
|
|
23478
|
+
*/
|
|
22598
23479
|
async lazyResume(channelId, threadId) {
|
|
22599
23480
|
const store = this.sessionStore;
|
|
22600
23481
|
if (!store || !this.createFullSession) return null;
|
|
@@ -22698,6 +23579,7 @@ var init_session_factory = __esm({
|
|
|
22698
23579
|
this.resumeLocks.set(lockKey, resumePromise);
|
|
22699
23580
|
return resumePromise;
|
|
22700
23581
|
}
|
|
23582
|
+
/** Create a brand-new session, resolving agent name and workspace from config if not provided. */
|
|
22701
23583
|
async handleNewSession(channelId, agentName, workspacePath, options) {
|
|
22702
23584
|
if (!this.configManager || !this.agentCatalog || !this.createFullSession) {
|
|
22703
23585
|
throw new Error("SessionFactory not fully initialized");
|
|
@@ -22742,6 +23624,7 @@ var init_session_factory = __esm({
|
|
|
22742
23624
|
record.workingDir
|
|
22743
23625
|
);
|
|
22744
23626
|
}
|
|
23627
|
+
/** Create a session and inject conversation context from a ContextProvider (e.g., history from a previous session). */
|
|
22745
23628
|
async createSessionWithContext(params) {
|
|
22746
23629
|
if (!this.createFullSession) throw new Error("SessionFactory not fully initialized");
|
|
22747
23630
|
let contextResult = null;
|
|
@@ -22768,6 +23651,7 @@ var init_session_factory = __esm({
|
|
|
22768
23651
|
}
|
|
22769
23652
|
return { session, contextResult };
|
|
22770
23653
|
}
|
|
23654
|
+
/** Wire session-level side effects: usage tracking (via EventBus) and tunnel cleanup on session end. */
|
|
22771
23655
|
wireSideEffects(session, deps) {
|
|
22772
23656
|
session.on(SessionEv.AGENT_EVENT, (event) => {
|
|
22773
23657
|
if (event.type !== "usage") return;
|
|
@@ -22814,7 +23698,12 @@ var init_agent_switch_handler = __esm({
|
|
|
22814
23698
|
constructor(deps) {
|
|
22815
23699
|
this.deps = deps;
|
|
22816
23700
|
}
|
|
23701
|
+
/** Prevents concurrent switch operations on the same session */
|
|
22817
23702
|
switchingLocks = /* @__PURE__ */ new Set();
|
|
23703
|
+
/**
|
|
23704
|
+
* Switch a session to a different agent. Returns whether the previous
|
|
23705
|
+
* agent session was resumed or a new one was spawned.
|
|
23706
|
+
*/
|
|
22818
23707
|
async switch(sessionId, toAgent) {
|
|
22819
23708
|
if (this.switchingLocks.has(sessionId)) {
|
|
22820
23709
|
throw new Error("Switch already in progress");
|
|
@@ -23025,20 +23914,29 @@ var init_agent_catalog = __esm({
|
|
|
23025
23914
|
DEFAULT_TTL_HOURS = 24;
|
|
23026
23915
|
AgentCatalog = class {
|
|
23027
23916
|
store;
|
|
23917
|
+
/** Agents available in the remote registry (cached in memory after load). */
|
|
23028
23918
|
registryAgents = [];
|
|
23029
23919
|
cachePath;
|
|
23920
|
+
/** Directory where binary agent archives are extracted to. */
|
|
23030
23921
|
agentsDir;
|
|
23031
23922
|
constructor(store, cachePath, agentsDir) {
|
|
23032
23923
|
this.store = store;
|
|
23033
23924
|
this.cachePath = cachePath;
|
|
23034
23925
|
this.agentsDir = agentsDir;
|
|
23035
23926
|
}
|
|
23927
|
+
/**
|
|
23928
|
+
* Load installed agents from disk and hydrate the registry from cache/snapshot.
|
|
23929
|
+
*
|
|
23930
|
+
* Also enriches installed agents with registry metadata — fixes agents that
|
|
23931
|
+
* were migrated from older config formats with incomplete data.
|
|
23932
|
+
*/
|
|
23036
23933
|
load() {
|
|
23037
23934
|
this.store.load();
|
|
23038
23935
|
this.loadRegistryFromCacheOrSnapshot();
|
|
23039
23936
|
this.enrichInstalledFromRegistry();
|
|
23040
23937
|
}
|
|
23041
23938
|
// --- Registry ---
|
|
23939
|
+
/** Fetch the latest agent registry from the CDN and update the local cache. */
|
|
23042
23940
|
async fetchRegistry() {
|
|
23043
23941
|
try {
|
|
23044
23942
|
log39.info("Fetching agent registry from CDN...");
|
|
@@ -23058,6 +23956,7 @@ var init_agent_catalog = __esm({
|
|
|
23058
23956
|
log39.warn({ err }, "Failed to fetch registry, using cached data");
|
|
23059
23957
|
}
|
|
23060
23958
|
}
|
|
23959
|
+
/** Re-fetch registry only if the local cache has expired (24-hour TTL). */
|
|
23061
23960
|
async refreshRegistryIfStale() {
|
|
23062
23961
|
if (this.isCacheStale()) {
|
|
23063
23962
|
await this.fetchRegistry();
|
|
@@ -23069,6 +23968,7 @@ var init_agent_catalog = __esm({
|
|
|
23069
23968
|
getRegistryAgent(registryId) {
|
|
23070
23969
|
return this.registryAgents.find((a) => a.id === registryId);
|
|
23071
23970
|
}
|
|
23971
|
+
/** Find a registry agent by registry ID or by its short alias (e.g., "claude"). */
|
|
23072
23972
|
findRegistryAgent(keyOrId) {
|
|
23073
23973
|
const byId = this.registryAgents.find((a) => a.id === keyOrId);
|
|
23074
23974
|
if (byId) return byId;
|
|
@@ -23085,6 +23985,15 @@ var init_agent_catalog = __esm({
|
|
|
23085
23985
|
return this.store.getAgent(key);
|
|
23086
23986
|
}
|
|
23087
23987
|
// --- Discovery ---
|
|
23988
|
+
/**
|
|
23989
|
+
* Build the unified list of all agents (installed + registry-only).
|
|
23990
|
+
*
|
|
23991
|
+
* Installed agents appear first with their live availability status.
|
|
23992
|
+
* Registry agents that aren't installed yet show whether a distribution
|
|
23993
|
+
* exists for the current platform. Missing external dependencies
|
|
23994
|
+
* (e.g., claude CLI) are surfaced as `missingDeps` for UI display
|
|
23995
|
+
* but do NOT block installation.
|
|
23996
|
+
*/
|
|
23088
23997
|
getAvailable() {
|
|
23089
23998
|
const installed = this.getInstalledEntries();
|
|
23090
23999
|
const items = [];
|
|
@@ -23125,6 +24034,7 @@ var init_agent_catalog = __esm({
|
|
|
23125
24034
|
}
|
|
23126
24035
|
return items;
|
|
23127
24036
|
}
|
|
24037
|
+
/** Check if an agent can be installed on this system (platform + dependencies). */
|
|
23128
24038
|
checkAvailability(keyOrId) {
|
|
23129
24039
|
const agent = this.findRegistryAgent(keyOrId);
|
|
23130
24040
|
if (!agent) return { available: false, reason: "Not found in the agent registry." };
|
|
@@ -23135,6 +24045,12 @@ var init_agent_catalog = __esm({
|
|
|
23135
24045
|
return checkDependencies(agent.id);
|
|
23136
24046
|
}
|
|
23137
24047
|
// --- Install/Uninstall ---
|
|
24048
|
+
/**
|
|
24049
|
+
* Install an agent from the registry.
|
|
24050
|
+
*
|
|
24051
|
+
* Resolves the distribution (npx/uvx/binary), downloads binary archives
|
|
24052
|
+
* if needed, and persists the agent definition in the store.
|
|
24053
|
+
*/
|
|
23138
24054
|
async install(keyOrId, progress, force) {
|
|
23139
24055
|
const agent = this.findRegistryAgent(keyOrId);
|
|
23140
24056
|
if (!agent) {
|
|
@@ -23159,6 +24075,7 @@ var init_agent_catalog = __esm({
|
|
|
23159
24075
|
registerFallbackAgent(key, data) {
|
|
23160
24076
|
this.store.addAgent(key, data);
|
|
23161
24077
|
}
|
|
24078
|
+
/** Remove an installed agent and delete its binary directory if applicable. */
|
|
23162
24079
|
async uninstall(key) {
|
|
23163
24080
|
if (this.store.hasAgent(key)) {
|
|
23164
24081
|
await uninstallAgent(key, this.store);
|
|
@@ -23167,6 +24084,7 @@ var init_agent_catalog = __esm({
|
|
|
23167
24084
|
return { ok: false, error: `"${key}" is not installed.` };
|
|
23168
24085
|
}
|
|
23169
24086
|
// --- Resolution (for AgentManager) ---
|
|
24087
|
+
/** Convert an installed agent's short key to an AgentDefinition for spawning. */
|
|
23170
24088
|
resolve(key) {
|
|
23171
24089
|
const agent = this.store.getAgent(key);
|
|
23172
24090
|
if (!agent) return void 0;
|
|
@@ -23310,6 +24228,7 @@ var init_agent_store = __esm({
|
|
|
23310
24228
|
constructor(filePath) {
|
|
23311
24229
|
this.filePath = filePath;
|
|
23312
24230
|
}
|
|
24231
|
+
/** Load and validate the store from disk. Starts fresh if file is missing or invalid. */
|
|
23313
24232
|
load() {
|
|
23314
24233
|
if (!fs44.existsSync(this.filePath)) {
|
|
23315
24234
|
this.data = { version: 1, installed: {} };
|
|
@@ -23349,6 +24268,11 @@ var init_agent_store = __esm({
|
|
|
23349
24268
|
hasAgent(key) {
|
|
23350
24269
|
return key in this.data.installed;
|
|
23351
24270
|
}
|
|
24271
|
+
/**
|
|
24272
|
+
* Persist the store to disk using atomic write (write to .tmp, then rename).
|
|
24273
|
+
* File permissions are restricted to owner-only (0o600) since the store
|
|
24274
|
+
* may contain agent binary paths and environment variables.
|
|
24275
|
+
*/
|
|
23352
24276
|
save() {
|
|
23353
24277
|
fs44.mkdirSync(path49.dirname(this.filePath), { recursive: true });
|
|
23354
24278
|
const tmpPath = this.filePath + ".tmp";
|
|
@@ -23446,6 +24370,10 @@ var init_service_registry = __esm({
|
|
|
23446
24370
|
"use strict";
|
|
23447
24371
|
ServiceRegistry = class {
|
|
23448
24372
|
services = /* @__PURE__ */ new Map();
|
|
24373
|
+
/**
|
|
24374
|
+
* Register a service. Throws if the service name is already taken.
|
|
24375
|
+
* Use `registerOverride` to intentionally replace an existing service.
|
|
24376
|
+
*/
|
|
23449
24377
|
register(name, implementation, pluginName) {
|
|
23450
24378
|
if (this.services.has(name)) {
|
|
23451
24379
|
const existing = this.services.get(name);
|
|
@@ -23453,21 +24381,27 @@ var init_service_registry = __esm({
|
|
|
23453
24381
|
}
|
|
23454
24382
|
this.services.set(name, { implementation, pluginName });
|
|
23455
24383
|
}
|
|
24384
|
+
/** Register a service, replacing any existing registration (used by override plugins). */
|
|
23456
24385
|
registerOverride(name, implementation, pluginName) {
|
|
23457
24386
|
this.services.set(name, { implementation, pluginName });
|
|
23458
24387
|
}
|
|
24388
|
+
/** Retrieve a service by name. Returns undefined if not registered. */
|
|
23459
24389
|
get(name) {
|
|
23460
24390
|
return this.services.get(name)?.implementation;
|
|
23461
24391
|
}
|
|
24392
|
+
/** Check whether a service is registered. */
|
|
23462
24393
|
has(name) {
|
|
23463
24394
|
return this.services.has(name);
|
|
23464
24395
|
}
|
|
24396
|
+
/** List all registered services with their owning plugin names. */
|
|
23465
24397
|
list() {
|
|
23466
24398
|
return [...this.services.entries()].map(([name, { pluginName }]) => ({ name, pluginName }));
|
|
23467
24399
|
}
|
|
24400
|
+
/** Remove a single service by name. */
|
|
23468
24401
|
unregister(name) {
|
|
23469
24402
|
this.services.delete(name);
|
|
23470
24403
|
}
|
|
24404
|
+
/** Remove all services owned by a specific plugin (called during plugin unload). */
|
|
23471
24405
|
unregisterByPlugin(pluginName) {
|
|
23472
24406
|
for (const [name, entry] of this.services) {
|
|
23473
24407
|
if (entry.pluginName === pluginName) {
|
|
@@ -23489,6 +24423,7 @@ var init_middleware_chain = __esm({
|
|
|
23489
24423
|
chains = /* @__PURE__ */ new Map();
|
|
23490
24424
|
errorHandler;
|
|
23491
24425
|
errorTracker;
|
|
24426
|
+
/** Register a middleware handler for a hook. Handlers are kept sorted by priority. */
|
|
23492
24427
|
add(hook, pluginName, opts) {
|
|
23493
24428
|
const entry = {
|
|
23494
24429
|
pluginName,
|
|
@@ -23503,6 +24438,15 @@ var init_middleware_chain = __esm({
|
|
|
23503
24438
|
this.chains.set(hook, [entry]);
|
|
23504
24439
|
}
|
|
23505
24440
|
}
|
|
24441
|
+
/**
|
|
24442
|
+
* Execute the middleware chain for a hook, ending with the core handler.
|
|
24443
|
+
*
|
|
24444
|
+
* The chain is built recursively: each handler calls `next()` to invoke the
|
|
24445
|
+
* next handler, with the core handler at the end. If no middleware is registered,
|
|
24446
|
+
* the core handler runs directly.
|
|
24447
|
+
*
|
|
24448
|
+
* @returns The final payload, or `null` if any handler short-circuited.
|
|
24449
|
+
*/
|
|
23506
24450
|
async execute(hook, payload, coreHandler) {
|
|
23507
24451
|
const handlers = this.chains.get(hook);
|
|
23508
24452
|
if (!handlers || handlers.length === 0) {
|
|
@@ -23572,6 +24516,7 @@ var init_middleware_chain = __esm({
|
|
|
23572
24516
|
const start = buildNext(0, payload);
|
|
23573
24517
|
return start();
|
|
23574
24518
|
}
|
|
24519
|
+
/** Remove all middleware handlers registered by a specific plugin. */
|
|
23575
24520
|
removeAll(pluginName) {
|
|
23576
24521
|
for (const [hook, handlers] of this.chains.entries()) {
|
|
23577
24522
|
const filtered = handlers.filter((h) => h.pluginName !== pluginName);
|
|
@@ -23582,9 +24527,11 @@ var init_middleware_chain = __esm({
|
|
|
23582
24527
|
}
|
|
23583
24528
|
}
|
|
23584
24529
|
}
|
|
24530
|
+
/** Set a callback for middleware errors (e.g., logging). */
|
|
23585
24531
|
setErrorHandler(fn) {
|
|
23586
24532
|
this.errorHandler = fn;
|
|
23587
24533
|
}
|
|
24534
|
+
/** Attach an ErrorTracker for circuit-breaking misbehaving plugins. */
|
|
23588
24535
|
setErrorTracker(tracker) {
|
|
23589
24536
|
this.errorTracker = tracker;
|
|
23590
24537
|
}
|
|
@@ -23602,10 +24549,15 @@ var init_error_tracker = __esm({
|
|
|
23602
24549
|
disabled = /* @__PURE__ */ new Set();
|
|
23603
24550
|
exempt = /* @__PURE__ */ new Set();
|
|
23604
24551
|
config;
|
|
24552
|
+
/** Callback fired when a plugin is auto-disabled due to error budget exhaustion. */
|
|
23605
24553
|
onDisabled;
|
|
23606
24554
|
constructor(config) {
|
|
23607
24555
|
this.config = { maxErrors: config?.maxErrors ?? 10, windowMs: config?.windowMs ?? 36e5 };
|
|
23608
24556
|
}
|
|
24557
|
+
/**
|
|
24558
|
+
* Record an error for a plugin. If the error budget is exceeded,
|
|
24559
|
+
* the plugin is disabled and the `onDisabled` callback fires.
|
|
24560
|
+
*/
|
|
23609
24561
|
increment(pluginName) {
|
|
23610
24562
|
if (this.exempt.has(pluginName)) return;
|
|
23611
24563
|
const now = Date.now();
|
|
@@ -23622,13 +24574,16 @@ var init_error_tracker = __esm({
|
|
|
23622
24574
|
this.onDisabled?.(pluginName, reason);
|
|
23623
24575
|
}
|
|
23624
24576
|
}
|
|
24577
|
+
/** Check if a plugin has been disabled due to errors. */
|
|
23625
24578
|
isDisabled(pluginName) {
|
|
23626
24579
|
return this.disabled.has(pluginName);
|
|
23627
24580
|
}
|
|
24581
|
+
/** Re-enable a plugin and clear its error history. */
|
|
23628
24582
|
reset(pluginName) {
|
|
23629
24583
|
this.disabled.delete(pluginName);
|
|
23630
24584
|
this.errors.delete(pluginName);
|
|
23631
24585
|
}
|
|
24586
|
+
/** Mark a plugin as exempt from circuit-breaking (e.g., essential plugins). */
|
|
23632
24587
|
setExempt(pluginName) {
|
|
23633
24588
|
this.exempt.add(pluginName);
|
|
23634
24589
|
}
|
|
@@ -23646,6 +24601,7 @@ var init_plugin_storage = __esm({
|
|
|
23646
24601
|
PluginStorageImpl = class {
|
|
23647
24602
|
kvPath;
|
|
23648
24603
|
dataDir;
|
|
24604
|
+
/** Serializes writes to prevent concurrent file corruption */
|
|
23649
24605
|
writeChain = Promise.resolve();
|
|
23650
24606
|
constructor(baseDir) {
|
|
23651
24607
|
this.dataDir = path50.join(baseDir, "data");
|
|
@@ -23686,6 +24642,7 @@ var init_plugin_storage = __esm({
|
|
|
23686
24642
|
async list() {
|
|
23687
24643
|
return Object.keys(this.readKv());
|
|
23688
24644
|
}
|
|
24645
|
+
/** Returns the plugin's data directory, creating it lazily on first access. */
|
|
23689
24646
|
getDataDir() {
|
|
23690
24647
|
fs45.mkdirSync(this.dataDir, { recursive: true });
|
|
23691
24648
|
return this.dataDir;
|
|
@@ -23861,6 +24818,11 @@ function createPluginContext(opts) {
|
|
|
23861
24818
|
return core;
|
|
23862
24819
|
},
|
|
23863
24820
|
instanceRoot,
|
|
24821
|
+
/**
|
|
24822
|
+
* Called by LifecycleManager during plugin teardown.
|
|
24823
|
+
* Unregisters all event handlers, middleware, commands, and services
|
|
24824
|
+
* registered by this plugin, preventing leaks across reloads.
|
|
24825
|
+
*/
|
|
23864
24826
|
cleanup() {
|
|
23865
24827
|
for (const { event, handler } of registeredListeners) {
|
|
23866
24828
|
eventBus.off(event, handler);
|
|
@@ -23967,12 +24929,15 @@ var init_lifecycle_manager = __esm({
|
|
|
23967
24929
|
loadOrder = [];
|
|
23968
24930
|
_loaded = /* @__PURE__ */ new Set();
|
|
23969
24931
|
_failed = /* @__PURE__ */ new Set();
|
|
24932
|
+
/** Names of plugins that successfully completed setup(). */
|
|
23970
24933
|
get loadedPlugins() {
|
|
23971
24934
|
return [...this._loaded];
|
|
23972
24935
|
}
|
|
24936
|
+
/** Names of plugins whose setup() threw an error. These plugins are skipped but don't crash the system. */
|
|
23973
24937
|
get failedPlugins() {
|
|
23974
24938
|
return [...this._failed];
|
|
23975
24939
|
}
|
|
24940
|
+
/** The PluginRegistry tracking installed and enabled plugin state. */
|
|
23976
24941
|
get registry() {
|
|
23977
24942
|
return this.pluginRegistry;
|
|
23978
24943
|
}
|
|
@@ -24019,6 +24984,12 @@ var init_lifecycle_manager = __esm({
|
|
|
24019
24984
|
return this;
|
|
24020
24985
|
} };
|
|
24021
24986
|
}
|
|
24987
|
+
/**
|
|
24988
|
+
* Boot a set of plugins in dependency order.
|
|
24989
|
+
*
|
|
24990
|
+
* Can be called multiple times (e.g., core plugins first, then dev plugins later).
|
|
24991
|
+
* Already-loaded plugins are included in dependency resolution but not re-booted.
|
|
24992
|
+
*/
|
|
24022
24993
|
async boot(plugins) {
|
|
24023
24994
|
const newNames = new Set(plugins.map((p2) => p2.name));
|
|
24024
24995
|
const allForResolution = [...this.loadOrder.filter((p2) => !newNames.has(p2.name)), ...plugins];
|
|
@@ -24130,6 +25101,11 @@ var init_lifecycle_manager = __esm({
|
|
|
24130
25101
|
}
|
|
24131
25102
|
}
|
|
24132
25103
|
}
|
|
25104
|
+
/**
|
|
25105
|
+
* Unload a single plugin: call teardown(), clean up its context
|
|
25106
|
+
* (listeners, middleware, services), and remove from tracked state.
|
|
25107
|
+
* Used for hot-reload: unload → rebuild → re-boot.
|
|
25108
|
+
*/
|
|
24133
25109
|
async unloadPlugin(name) {
|
|
24134
25110
|
if (!this._loaded.has(name)) return;
|
|
24135
25111
|
const plugin2 = this.loadOrder.find((p2) => p2.name === name);
|
|
@@ -24149,6 +25125,10 @@ var init_lifecycle_manager = __esm({
|
|
|
24149
25125
|
this.loadOrder = this.loadOrder.filter((p2) => p2.name !== name);
|
|
24150
25126
|
this.eventBus?.emit(BusEvent.PLUGIN_UNLOADED, { name });
|
|
24151
25127
|
}
|
|
25128
|
+
/**
|
|
25129
|
+
* Gracefully shut down all loaded plugins.
|
|
25130
|
+
* Teardown runs in reverse boot order so that dependencies outlive their dependents.
|
|
25131
|
+
*/
|
|
24152
25132
|
async shutdown() {
|
|
24153
25133
|
const reversed = [...this.loadOrder].reverse();
|
|
24154
25134
|
for (const plugin2 of reversed) {
|
|
@@ -24182,16 +25162,19 @@ var init_menu_registry = __esm({
|
|
|
24182
25162
|
log41 = createChildLogger({ module: "menu-registry" });
|
|
24183
25163
|
MenuRegistry = class {
|
|
24184
25164
|
items = /* @__PURE__ */ new Map();
|
|
25165
|
+
/** Register or replace a menu item by its unique ID. */
|
|
24185
25166
|
register(item) {
|
|
24186
25167
|
this.items.set(item.id, item);
|
|
24187
25168
|
}
|
|
25169
|
+
/** Remove a menu item by ID. */
|
|
24188
25170
|
unregister(id) {
|
|
24189
25171
|
this.items.delete(id);
|
|
24190
25172
|
}
|
|
25173
|
+
/** Look up a single menu item by ID. */
|
|
24191
25174
|
getItem(id) {
|
|
24192
25175
|
return this.items.get(id);
|
|
24193
25176
|
}
|
|
24194
|
-
/** Get all visible items sorted by priority */
|
|
25177
|
+
/** Get all visible items sorted by priority (lower number = shown first). */
|
|
24195
25178
|
getItems() {
|
|
24196
25179
|
return [...this.items.values()].filter((item) => {
|
|
24197
25180
|
if (!item.visible) return true;
|
|
@@ -24281,19 +25264,31 @@ var init_assistant_registry = __esm({
|
|
|
24281
25264
|
AssistantRegistry = class {
|
|
24282
25265
|
sections = /* @__PURE__ */ new Map();
|
|
24283
25266
|
_instanceRoot = "";
|
|
24284
|
-
/** Set the instance root path used in assistant guidelines */
|
|
25267
|
+
/** Set the instance root path used in assistant guidelines. */
|
|
24285
25268
|
setInstanceRoot(root) {
|
|
24286
25269
|
this._instanceRoot = root;
|
|
24287
25270
|
}
|
|
25271
|
+
/** Register a prompt section. Overwrites any existing section with the same id. */
|
|
24288
25272
|
register(section) {
|
|
24289
25273
|
if (this.sections.has(section.id)) {
|
|
24290
25274
|
log42.warn({ id: section.id }, "Assistant section overwritten");
|
|
24291
25275
|
}
|
|
24292
25276
|
this.sections.set(section.id, section);
|
|
24293
25277
|
}
|
|
25278
|
+
/** Remove a previously registered section by id. */
|
|
24294
25279
|
unregister(id) {
|
|
24295
25280
|
this.sections.delete(id);
|
|
24296
25281
|
}
|
|
25282
|
+
/**
|
|
25283
|
+
* Compose the full system prompt from all registered sections.
|
|
25284
|
+
*
|
|
25285
|
+
* Sections are sorted by priority (ascending), each contributing a titled
|
|
25286
|
+
* markdown block. If a section's `buildContext()` throws, it is skipped
|
|
25287
|
+
* gracefully so one broken section doesn't break the entire prompt.
|
|
25288
|
+
*
|
|
25289
|
+
* If `channelId` is provided, a "Current Channel" block is injected at the
|
|
25290
|
+
* top of the prompt so the assistant can adapt its behavior to the platform.
|
|
25291
|
+
*/
|
|
24297
25292
|
buildSystemPrompt(channelId) {
|
|
24298
25293
|
const sorted = [...this.sections.values()].sort((a, b) => a.priority - b.priority);
|
|
24299
25294
|
const parts = [ASSISTANT_PREAMBLE];
|
|
@@ -24336,6 +25331,13 @@ var init_assistant_manager = __esm({
|
|
|
24336
25331
|
}
|
|
24337
25332
|
sessions = /* @__PURE__ */ new Map();
|
|
24338
25333
|
pendingSystemPrompts = /* @__PURE__ */ new Map();
|
|
25334
|
+
/**
|
|
25335
|
+
* Returns the assistant session for a channel, creating one if needed.
|
|
25336
|
+
*
|
|
25337
|
+
* If a persisted assistant session exists in the store, it is reused
|
|
25338
|
+
* (same session ID) to preserve conversation history. The system prompt
|
|
25339
|
+
* is always rebuilt fresh and deferred until the first user message.
|
|
25340
|
+
*/
|
|
24339
25341
|
async getOrSpawn(channelId, threadId) {
|
|
24340
25342
|
const existing = this.core.sessionStore?.findAssistant(channelId);
|
|
24341
25343
|
const session = await this.core.createSession({
|
|
@@ -24356,6 +25358,7 @@ var init_assistant_manager = __esm({
|
|
|
24356
25358
|
);
|
|
24357
25359
|
return session;
|
|
24358
25360
|
}
|
|
25361
|
+
/** Returns the active assistant session for a channel, or null if none exists. */
|
|
24359
25362
|
get(channelId) {
|
|
24360
25363
|
return this.sessions.get(channelId) ?? null;
|
|
24361
25364
|
}
|
|
@@ -24368,6 +25371,7 @@ var init_assistant_manager = __esm({
|
|
|
24368
25371
|
if (prompt) this.pendingSystemPrompts.delete(channelId);
|
|
24369
25372
|
return prompt;
|
|
24370
25373
|
}
|
|
25374
|
+
/** Checks whether a given session ID belongs to the built-in assistant. */
|
|
24371
25375
|
isAssistant(sessionId) {
|
|
24372
25376
|
for (const s of this.sessions.values()) {
|
|
24373
25377
|
if (s.id === sessionId) return true;
|
|
@@ -24666,30 +25670,49 @@ var init_core = __esm({
|
|
|
24666
25670
|
menuRegistry = new MenuRegistry();
|
|
24667
25671
|
assistantRegistry = new AssistantRegistry();
|
|
24668
25672
|
assistantManager;
|
|
24669
|
-
//
|
|
25673
|
+
// Services (security, notifications, speech, etc.) are provided by plugins that
|
|
25674
|
+
// register during boot. Core accesses them lazily via ServiceRegistry so it doesn't
|
|
25675
|
+
// need compile-time dependencies on plugin implementations.
|
|
25676
|
+
/** @throws if the service hasn't been registered by its plugin yet */
|
|
24670
25677
|
getService(name) {
|
|
24671
25678
|
const svc = this.lifecycleManager.serviceRegistry.get(name);
|
|
24672
25679
|
if (!svc) throw new Error(`Service '${name}' not registered \u2014 is the ${name} plugin loaded?`);
|
|
24673
25680
|
return svc;
|
|
24674
25681
|
}
|
|
25682
|
+
/** Access control and rate-limiting guard (provided by security plugin). */
|
|
24675
25683
|
get securityGuard() {
|
|
24676
25684
|
return this.getService("security");
|
|
24677
25685
|
}
|
|
25686
|
+
/** Cross-session notification delivery (provided by notifications plugin). */
|
|
24678
25687
|
get notificationManager() {
|
|
24679
25688
|
return this.getService("notifications");
|
|
24680
25689
|
}
|
|
25690
|
+
/** File I/O service for agent attachment storage (provided by file-service plugin). */
|
|
24681
25691
|
get fileService() {
|
|
24682
25692
|
return this.getService("file-service");
|
|
24683
25693
|
}
|
|
25694
|
+
/** Text-to-speech / speech-to-text engine (provided by speech plugin). */
|
|
24684
25695
|
get speechService() {
|
|
24685
25696
|
return this.getService("speech");
|
|
24686
25697
|
}
|
|
25698
|
+
/** Conversation history builder for context injection (provided by context plugin). */
|
|
24687
25699
|
get contextManager() {
|
|
24688
25700
|
return this.getService("context");
|
|
24689
25701
|
}
|
|
25702
|
+
/** Per-plugin persistent settings (e.g. API keys). */
|
|
24690
25703
|
get settingsManager() {
|
|
24691
25704
|
return this.lifecycleManager.settingsManager;
|
|
24692
25705
|
}
|
|
25706
|
+
/**
|
|
25707
|
+
* Bootstrap all core subsystems. The boot order matters:
|
|
25708
|
+
* 1. AgentCatalog + AgentManager (agent definitions)
|
|
25709
|
+
* 2. SessionStore + SessionManager (session persistence and lookup)
|
|
25710
|
+
* 3. EventBus (inter-module communication)
|
|
25711
|
+
* 4. SessionFactory (session creation pipeline)
|
|
25712
|
+
* 5. LifecycleManager (plugin infrastructure)
|
|
25713
|
+
* 6. Wire middleware chain into factory + manager
|
|
25714
|
+
* 7. AgentSwitchHandler, config listeners, menu/assistant registries
|
|
25715
|
+
*/
|
|
24693
25716
|
constructor(configManager, ctx) {
|
|
24694
25717
|
this.configManager = configManager;
|
|
24695
25718
|
this.instanceContext = ctx;
|
|
@@ -24800,16 +25823,28 @@ var init_core = __esm({
|
|
|
24800
25823
|
this.lifecycleManager.serviceRegistry.register("menu-registry", this.menuRegistry, "core");
|
|
24801
25824
|
this.lifecycleManager.serviceRegistry.register("assistant-registry", this.assistantRegistry, "core");
|
|
24802
25825
|
}
|
|
25826
|
+
/** Optional tunnel for generating public URLs (code viewer links, etc.). */
|
|
24803
25827
|
get tunnelService() {
|
|
24804
25828
|
return this._tunnelService;
|
|
24805
25829
|
}
|
|
25830
|
+
/** Propagate tunnel service to MessageTransformer so it can generate viewer links. */
|
|
24806
25831
|
set tunnelService(service) {
|
|
24807
25832
|
this._tunnelService = service;
|
|
24808
25833
|
this.messageTransformer.tunnelService = service;
|
|
24809
25834
|
}
|
|
25835
|
+
/**
|
|
25836
|
+
* Register a messaging adapter (e.g. Telegram, Slack, SSE).
|
|
25837
|
+
*
|
|
25838
|
+
* Adapters must be registered before `start()`. The adapter name serves as its
|
|
25839
|
+
* channel ID throughout the system — used in session records, bridge keys, and routing.
|
|
25840
|
+
*/
|
|
24810
25841
|
registerAdapter(name, adapter) {
|
|
24811
25842
|
this.adapters.set(name, adapter);
|
|
24812
25843
|
}
|
|
25844
|
+
/**
|
|
25845
|
+
* Start all registered adapters. Adapters that fail are logged but do not
|
|
25846
|
+
* prevent others from starting. Throws only if ALL adapters fail.
|
|
25847
|
+
*/
|
|
24813
25848
|
async start() {
|
|
24814
25849
|
this.agentCatalog.refreshRegistryIfStale().catch((err) => {
|
|
24815
25850
|
log44.warn({ err }, "Background registry refresh failed");
|
|
@@ -24829,6 +25864,10 @@ var init_core = __esm({
|
|
|
24829
25864
|
);
|
|
24830
25865
|
}
|
|
24831
25866
|
}
|
|
25867
|
+
/**
|
|
25868
|
+
* Graceful shutdown: notify users, persist session state, stop adapters.
|
|
25869
|
+
* Agent subprocesses are not explicitly killed — they exit with the parent process.
|
|
25870
|
+
*/
|
|
24832
25871
|
async stop() {
|
|
24833
25872
|
try {
|
|
24834
25873
|
const nm = this.lifecycleManager.serviceRegistry.get("notifications");
|
|
@@ -24847,6 +25886,13 @@ var init_core = __esm({
|
|
|
24847
25886
|
}
|
|
24848
25887
|
}
|
|
24849
25888
|
// --- Archive ---
|
|
25889
|
+
/**
|
|
25890
|
+
* Archive a session: delete its adapter topic/thread and cancel the session.
|
|
25891
|
+
*
|
|
25892
|
+
* Only sessions in archivable states (active, cancelled, error) can be archived —
|
|
25893
|
+
* initializing and finished sessions are excluded.
|
|
25894
|
+
* The adapter handles platform-side cleanup (e.g. deleting a Telegram topic).
|
|
25895
|
+
*/
|
|
24850
25896
|
async archiveSession(sessionId) {
|
|
24851
25897
|
const session = this.sessionManager.getSession(sessionId);
|
|
24852
25898
|
if (!session) return { ok: false, error: "Session not found (must be in memory)" };
|
|
@@ -24866,6 +25912,18 @@ var init_core = __esm({
|
|
|
24866
25912
|
}
|
|
24867
25913
|
}
|
|
24868
25914
|
// --- Message Routing ---
|
|
25915
|
+
/**
|
|
25916
|
+
* Route an incoming platform message to the appropriate session.
|
|
25917
|
+
*
|
|
25918
|
+
* Flow:
|
|
25919
|
+
* 1. Run `message:incoming` middleware (plugins can modify or block)
|
|
25920
|
+
* 2. SecurityGuard checks user access and per-user session limits
|
|
25921
|
+
* 3. Find session by channel+thread (in-memory lookup, then lazy resume from disk)
|
|
25922
|
+
* 4. For assistant sessions, prepend any deferred system prompt
|
|
25923
|
+
* 5. Emit `message:queued` for SSE clients, then enqueue the prompt on the session
|
|
25924
|
+
*
|
|
25925
|
+
* If no session is found, the user is told to start one with /new.
|
|
25926
|
+
*/
|
|
24869
25927
|
async handleMessage(message) {
|
|
24870
25928
|
log44.debug(
|
|
24871
25929
|
{
|
|
@@ -24947,6 +26005,17 @@ ${text5}`;
|
|
|
24947
26005
|
}
|
|
24948
26006
|
}
|
|
24949
26007
|
// --- Unified Session Creation Pipeline ---
|
|
26008
|
+
/**
|
|
26009
|
+
* Create (or resume) a session with full wiring: agent, adapter thread, bridge, persistence.
|
|
26010
|
+
*
|
|
26011
|
+
* This is the single entry point for session creation. The pipeline:
|
|
26012
|
+
* 1. SessionFactory spawns/resumes the agent process
|
|
26013
|
+
* 2. Adapter creates a thread/topic if requested
|
|
26014
|
+
* 3. Initial session record is persisted (so lazy resume can find it by threadId)
|
|
26015
|
+
* 4. SessionBridge connects agent events to the adapter
|
|
26016
|
+
* 5. For headless sessions (no adapter), fallback event handlers are wired inline
|
|
26017
|
+
* 6. Side effects (usage tracking, tunnel cleanup) are attached
|
|
26018
|
+
*/
|
|
24950
26019
|
async createSession(params) {
|
|
24951
26020
|
const session = await this.sessionFactory.create(params);
|
|
24952
26021
|
if (params.threadId) {
|
|
@@ -25069,9 +26138,16 @@ ${text5}`;
|
|
|
25069
26138
|
);
|
|
25070
26139
|
return session;
|
|
25071
26140
|
}
|
|
26141
|
+
/** Convenience wrapper: create a new session with default agent/workspace resolution. */
|
|
25072
26142
|
async handleNewSession(channelId, agentName, workspacePath, options) {
|
|
25073
26143
|
return this.sessionFactory.handleNewSession(channelId, agentName, workspacePath, options);
|
|
25074
26144
|
}
|
|
26145
|
+
/**
|
|
26146
|
+
* Adopt an externally-started agent session (e.g. from a CLI `openacp adopt` command).
|
|
26147
|
+
*
|
|
26148
|
+
* Validates that the agent supports resume, checks session limits, avoids duplicates,
|
|
26149
|
+
* then creates a full session via the unified pipeline with resume semantics.
|
|
26150
|
+
*/
|
|
25075
26151
|
async adoptSession(agentName, agentSessionId, cwd, channelId) {
|
|
25076
26152
|
const caps = getAgentCapabilities(agentName);
|
|
25077
26153
|
if (!caps.supportsResume) {
|
|
@@ -25182,22 +26258,33 @@ ${text5}`;
|
|
|
25182
26258
|
status: "adopted"
|
|
25183
26259
|
};
|
|
25184
26260
|
}
|
|
26261
|
+
/** Start a new chat within the same agent and workspace as the current session's thread. */
|
|
25185
26262
|
async handleNewChat(channelId, currentThreadId) {
|
|
25186
26263
|
return this.sessionFactory.handleNewChat(channelId, currentThreadId);
|
|
25187
26264
|
}
|
|
26265
|
+
/** Create a session and inject conversation context from a prior session or repo. */
|
|
25188
26266
|
async createSessionWithContext(params) {
|
|
25189
26267
|
return this.sessionFactory.createSessionWithContext(params);
|
|
25190
26268
|
}
|
|
25191
26269
|
// --- Agent Switch ---
|
|
26270
|
+
/** Switch a session's active agent. Delegates to AgentSwitchHandler for state coordination. */
|
|
25192
26271
|
async switchSessionAgent(sessionId, toAgent) {
|
|
25193
26272
|
return this.agentSwitchHandler.switch(sessionId, toAgent);
|
|
25194
26273
|
}
|
|
26274
|
+
/** Find a session by channel+thread, resuming from disk if not in memory. */
|
|
25195
26275
|
async getOrResumeSession(channelId, threadId) {
|
|
25196
26276
|
return this.sessionFactory.getOrResume(channelId, threadId);
|
|
25197
26277
|
}
|
|
26278
|
+
/** Find a session by ID, resuming from disk if not in memory. */
|
|
25198
26279
|
async getOrResumeSessionById(sessionId) {
|
|
25199
26280
|
return this.sessionFactory.getOrResumeById(sessionId);
|
|
25200
26281
|
}
|
|
26282
|
+
/**
|
|
26283
|
+
* Attach an additional adapter to an existing session (multi-adapter support).
|
|
26284
|
+
*
|
|
26285
|
+
* Creates a thread on the target adapter and connects a SessionBridge so the
|
|
26286
|
+
* session's agent events are forwarded to both the primary and attached adapters.
|
|
26287
|
+
*/
|
|
25201
26288
|
async attachAdapter(sessionId, adapterId) {
|
|
25202
26289
|
const session = this.sessionManager.getSession(sessionId);
|
|
25203
26290
|
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
@@ -25221,6 +26308,10 @@ ${text5}`;
|
|
|
25221
26308
|
});
|
|
25222
26309
|
return { threadId };
|
|
25223
26310
|
}
|
|
26311
|
+
/**
|
|
26312
|
+
* Detach a secondary adapter from a session. The primary adapter (channelId) cannot
|
|
26313
|
+
* be detached. Disconnects the bridge and cleans up thread mappings.
|
|
26314
|
+
*/
|
|
25224
26315
|
async detachAdapter(sessionId, adapterId) {
|
|
25225
26316
|
const session = this.sessionManager.getSession(sessionId);
|
|
25226
26317
|
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
@@ -25253,6 +26344,7 @@ ${text5}`;
|
|
|
25253
26344
|
platforms: this.buildPlatformsFromSession(session)
|
|
25254
26345
|
});
|
|
25255
26346
|
}
|
|
26347
|
+
/** Build the platforms map (adapter → thread/topic IDs) for persistence. */
|
|
25256
26348
|
buildPlatformsFromSession(session) {
|
|
25257
26349
|
const platforms = {};
|
|
25258
26350
|
for (const [adapterId, threadId] of session.threadIds) {
|
|
@@ -25284,8 +26376,13 @@ ${text5}`;
|
|
|
25284
26376
|
const bridge = this.createBridge(session, adapter, session.channelId);
|
|
25285
26377
|
bridge.connect();
|
|
25286
26378
|
}
|
|
25287
|
-
/**
|
|
25288
|
-
*
|
|
26379
|
+
/**
|
|
26380
|
+
* Create a SessionBridge for the given session and adapter.
|
|
26381
|
+
*
|
|
26382
|
+
* The bridge subscribes to Session events (agent output, status changes, naming)
|
|
26383
|
+
* and forwards them to the adapter for platform delivery. Disconnects any existing
|
|
26384
|
+
* bridge for the same adapter+session first to avoid duplicate event handlers.
|
|
26385
|
+
*/
|
|
25289
26386
|
createBridge(session, adapter, adapterId) {
|
|
25290
26387
|
const id = adapterId ?? adapter.name;
|
|
25291
26388
|
const key = this.bridgeKey(id, session.id);
|
|
@@ -25395,10 +26492,15 @@ var init_command_registry = __esm({
|
|
|
25395
26492
|
return this.getAll().filter((cmd) => cmd.category === category);
|
|
25396
26493
|
}
|
|
25397
26494
|
/**
|
|
25398
|
-
* Parse and execute a command string.
|
|
25399
|
-
*
|
|
25400
|
-
*
|
|
25401
|
-
*
|
|
26495
|
+
* Parse and execute a command string (e.g. "/greet hello world").
|
|
26496
|
+
*
|
|
26497
|
+
* Resolution order:
|
|
26498
|
+
* 1. Adapter-specific override (e.g. Telegram's version of /new)
|
|
26499
|
+
* 2. Short name or qualified name in the main registry
|
|
26500
|
+
*
|
|
26501
|
+
* Strips Telegram-style bot mentions (e.g. "/help@MyBot" → "help").
|
|
26502
|
+
* Returns `{ type: 'delegated' }` if the handler returns null/undefined
|
|
26503
|
+
* (meaning it handled the response itself, e.g. via assistant).
|
|
25402
26504
|
*/
|
|
25403
26505
|
async execute(commandString, baseArgs) {
|
|
25404
26506
|
const trimmed = commandString.trim();
|
|
@@ -26193,12 +27295,15 @@ var init_plugin_field_registry = __esm({
|
|
|
26193
27295
|
"use strict";
|
|
26194
27296
|
PluginFieldRegistry = class {
|
|
26195
27297
|
fields = /* @__PURE__ */ new Map();
|
|
27298
|
+
/** Register (or replace) the editable fields for a plugin. */
|
|
26196
27299
|
register(pluginName, fields) {
|
|
26197
27300
|
this.fields.set(pluginName, fields);
|
|
26198
27301
|
}
|
|
27302
|
+
/** Get the editable fields for a specific plugin. */
|
|
26199
27303
|
getForPlugin(pluginName) {
|
|
26200
27304
|
return this.fields.get(pluginName) ?? [];
|
|
26201
27305
|
}
|
|
27306
|
+
/** Get all fields grouped by plugin name. */
|
|
26202
27307
|
getAll() {
|
|
26203
27308
|
return new Map(this.fields);
|
|
26204
27309
|
}
|
|
@@ -27555,6 +28660,10 @@ var init_dev_loader = __esm({
|
|
|
27555
28660
|
constructor(pluginPath) {
|
|
27556
28661
|
this.pluginPath = path55.resolve(pluginPath);
|
|
27557
28662
|
}
|
|
28663
|
+
/**
|
|
28664
|
+
* Import the plugin's default export from dist/index.js.
|
|
28665
|
+
* Each call uses a unique URL query to bypass Node's ESM cache.
|
|
28666
|
+
*/
|
|
27558
28667
|
async load() {
|
|
27559
28668
|
const distIndex = path55.join(this.pluginPath, "dist", "index.js");
|
|
27560
28669
|
const srcIndex = path55.join(this.pluginPath, "src", "index.ts");
|
|
@@ -27572,9 +28681,11 @@ var init_dev_loader = __esm({
|
|
|
27572
28681
|
}
|
|
27573
28682
|
return plugin2;
|
|
27574
28683
|
}
|
|
28684
|
+
/** Returns the resolved absolute path to the plugin's root directory. */
|
|
27575
28685
|
getPluginPath() {
|
|
27576
28686
|
return this.pluginPath;
|
|
27577
28687
|
}
|
|
28688
|
+
/** Returns the path to the plugin's dist directory. */
|
|
27578
28689
|
getDistPath() {
|
|
27579
28690
|
return path55.join(this.pluginPath, "dist");
|
|
27580
28691
|
}
|