@openacp/cli 2026.410.1 → 2026.410.3
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 +1209 -77
- 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];
|
|
@@ -8292,32 +8561,48 @@ async function sessionRoutes(app, deps) {
|
|
|
8292
8561
|
{ preHandler: requireScopes("sessions:read") },
|
|
8293
8562
|
async (request) => {
|
|
8294
8563
|
const { sessionId } = SessionIdParamSchema.parse(request.params);
|
|
8295
|
-
const
|
|
8296
|
-
|
|
8297
|
-
)
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8564
|
+
const id = decodeURIComponent(sessionId);
|
|
8565
|
+
const session = deps.core.sessionManager.getSession(id);
|
|
8566
|
+
if (session) {
|
|
8567
|
+
return {
|
|
8568
|
+
session: {
|
|
8569
|
+
id: session.id,
|
|
8570
|
+
agent: session.agentName,
|
|
8571
|
+
status: session.status,
|
|
8572
|
+
name: session.name ?? null,
|
|
8573
|
+
workspace: session.workingDirectory,
|
|
8574
|
+
createdAt: session.createdAt.toISOString(),
|
|
8575
|
+
dangerousMode: session.clientOverrides.bypassPermissions ?? false,
|
|
8576
|
+
queueDepth: session.queueDepth,
|
|
8577
|
+
promptRunning: session.promptRunning,
|
|
8578
|
+
threadId: session.threadId,
|
|
8579
|
+
channelId: session.channelId,
|
|
8580
|
+
agentSessionId: session.agentSessionId,
|
|
8581
|
+
configOptions: session.configOptions?.length ? session.configOptions : void 0,
|
|
8582
|
+
capabilities: session.agentCapabilities ?? null
|
|
8583
|
+
}
|
|
8584
|
+
};
|
|
8585
|
+
}
|
|
8586
|
+
const record = deps.core.sessionManager.getSessionRecord(id);
|
|
8587
|
+
if (!record) {
|
|
8588
|
+
throw new NotFoundError("SESSION_NOT_FOUND", `Session "${id}" not found`);
|
|
8303
8589
|
}
|
|
8304
8590
|
return {
|
|
8305
8591
|
session: {
|
|
8306
|
-
id:
|
|
8307
|
-
agent:
|
|
8308
|
-
status:
|
|
8309
|
-
name:
|
|
8310
|
-
workspace:
|
|
8311
|
-
createdAt:
|
|
8312
|
-
dangerousMode:
|
|
8313
|
-
queueDepth:
|
|
8314
|
-
promptRunning:
|
|
8315
|
-
threadId:
|
|
8316
|
-
channelId:
|
|
8317
|
-
agentSessionId:
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
capabilities: session.agentCapabilities ?? null
|
|
8592
|
+
id: record.sessionId,
|
|
8593
|
+
agent: record.agentName,
|
|
8594
|
+
status: record.status,
|
|
8595
|
+
name: record.name ?? null,
|
|
8596
|
+
workspace: record.workingDir,
|
|
8597
|
+
createdAt: record.createdAt,
|
|
8598
|
+
dangerousMode: record.clientOverrides?.bypassPermissions ?? false,
|
|
8599
|
+
queueDepth: 0,
|
|
8600
|
+
promptRunning: false,
|
|
8601
|
+
threadId: null,
|
|
8602
|
+
channelId: record.channelId,
|
|
8603
|
+
agentSessionId: record.agentSessionId,
|
|
8604
|
+
configOptions: record.acpState?.configOptions?.length ? record.acpState.configOptions : void 0,
|
|
8605
|
+
capabilities: record.acpState?.agentCapabilities ?? null
|
|
8321
8606
|
}
|
|
8322
8607
|
};
|
|
8323
8608
|
}
|
|
@@ -8452,7 +8737,7 @@ async function sessionRoutes(app, deps) {
|
|
|
8452
8737
|
async (request, reply) => {
|
|
8453
8738
|
const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
|
|
8454
8739
|
const sessionId = decodeURIComponent(rawId);
|
|
8455
|
-
const session = deps.core.
|
|
8740
|
+
const session = await deps.core.getOrResumeSessionById(sessionId);
|
|
8456
8741
|
if (!session) {
|
|
8457
8742
|
throw new NotFoundError(
|
|
8458
8743
|
"SESSION_NOT_FOUND",
|
|
@@ -8481,7 +8766,7 @@ async function sessionRoutes(app, deps) {
|
|
|
8481
8766
|
async (request) => {
|
|
8482
8767
|
const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
|
|
8483
8768
|
const sessionId = decodeURIComponent(rawId);
|
|
8484
|
-
const session = deps.core.
|
|
8769
|
+
const session = await deps.core.getOrResumeSessionById(sessionId);
|
|
8485
8770
|
if (!session) {
|
|
8486
8771
|
throw new NotFoundError(
|
|
8487
8772
|
"SESSION_NOT_FOUND",
|
|
@@ -8518,7 +8803,7 @@ async function sessionRoutes(app, deps) {
|
|
|
8518
8803
|
async (request) => {
|
|
8519
8804
|
const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
|
|
8520
8805
|
const sessionId = decodeURIComponent(rawId);
|
|
8521
|
-
const session = deps.core.
|
|
8806
|
+
const session = await deps.core.getOrResumeSessionById(sessionId);
|
|
8522
8807
|
if (!session) {
|
|
8523
8808
|
throw new NotFoundError(
|
|
8524
8809
|
"SESSION_NOT_FOUND",
|
|
@@ -8540,15 +8825,19 @@ async function sessionRoutes(app, deps) {
|
|
|
8540
8825
|
const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
|
|
8541
8826
|
const sessionId = decodeURIComponent(rawId);
|
|
8542
8827
|
const session = deps.core.sessionManager.getSession(sessionId);
|
|
8543
|
-
if (
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8828
|
+
if (session) {
|
|
8829
|
+
return {
|
|
8830
|
+
configOptions: session.configOptions,
|
|
8831
|
+
clientOverrides: session.clientOverrides
|
|
8832
|
+
};
|
|
8833
|
+
}
|
|
8834
|
+
const record = deps.core.sessionManager.getSessionRecord(sessionId);
|
|
8835
|
+
if (!record) {
|
|
8836
|
+
throw new NotFoundError("SESSION_NOT_FOUND", `Session "${sessionId}" not found`);
|
|
8548
8837
|
}
|
|
8549
8838
|
return {
|
|
8550
|
-
configOptions:
|
|
8551
|
-
clientOverrides:
|
|
8839
|
+
configOptions: record.acpState?.configOptions,
|
|
8840
|
+
clientOverrides: record.clientOverrides ?? {}
|
|
8552
8841
|
};
|
|
8553
8842
|
}
|
|
8554
8843
|
);
|
|
@@ -8558,7 +8847,7 @@ async function sessionRoutes(app, deps) {
|
|
|
8558
8847
|
async (request) => {
|
|
8559
8848
|
const { sessionId: rawId, configId } = ConfigIdParamSchema.parse(request.params);
|
|
8560
8849
|
const sessionId = decodeURIComponent(rawId);
|
|
8561
|
-
const session = deps.core.
|
|
8850
|
+
const session = await deps.core.getOrResumeSessionById(sessionId);
|
|
8562
8851
|
if (!session) {
|
|
8563
8852
|
throw new NotFoundError(
|
|
8564
8853
|
"SESSION_NOT_FOUND",
|
|
@@ -8583,13 +8872,14 @@ async function sessionRoutes(app, deps) {
|
|
|
8583
8872
|
const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
|
|
8584
8873
|
const sessionId = decodeURIComponent(rawId);
|
|
8585
8874
|
const session = deps.core.sessionManager.getSession(sessionId);
|
|
8586
|
-
if (
|
|
8587
|
-
|
|
8588
|
-
"SESSION_NOT_FOUND",
|
|
8589
|
-
`Session "${sessionId}" not found`
|
|
8590
|
-
);
|
|
8875
|
+
if (session) {
|
|
8876
|
+
return { clientOverrides: session.clientOverrides };
|
|
8591
8877
|
}
|
|
8592
|
-
|
|
8878
|
+
const record = deps.core.sessionManager.getSessionRecord(sessionId);
|
|
8879
|
+
if (!record) {
|
|
8880
|
+
throw new NotFoundError("SESSION_NOT_FOUND", `Session "${sessionId}" not found`);
|
|
8881
|
+
}
|
|
8882
|
+
return { clientOverrides: record.clientOverrides ?? {} };
|
|
8593
8883
|
}
|
|
8594
8884
|
);
|
|
8595
8885
|
app.put(
|
|
@@ -8598,7 +8888,7 @@ async function sessionRoutes(app, deps) {
|
|
|
8598
8888
|
async (request) => {
|
|
8599
8889
|
const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
|
|
8600
8890
|
const sessionId = decodeURIComponent(rawId);
|
|
8601
|
-
const session = deps.core.
|
|
8891
|
+
const session = await deps.core.getOrResumeSessionById(sessionId);
|
|
8602
8892
|
if (!session) {
|
|
8603
8893
|
throw new NotFoundError(
|
|
8604
8894
|
"SESSION_NOT_FOUND",
|
|
@@ -8664,8 +8954,8 @@ async function sessionRoutes(app, deps) {
|
|
|
8664
8954
|
{ preHandler: requireScopes("sessions:read") },
|
|
8665
8955
|
async (request, reply) => {
|
|
8666
8956
|
const { sessionId } = SessionIdParamSchema.parse(request.params);
|
|
8667
|
-
const
|
|
8668
|
-
if (!
|
|
8957
|
+
const isKnown = deps.core.sessionManager.getSession(sessionId) ?? deps.core.sessionManager.getSessionRecord(sessionId);
|
|
8958
|
+
if (!isKnown) {
|
|
8669
8959
|
throw new NotFoundError(
|
|
8670
8960
|
"SESSION_NOT_FOUND",
|
|
8671
8961
|
`Session "${sessionId}" not found`
|
|
@@ -8687,8 +8977,8 @@ async function sessionRoutes(app, deps) {
|
|
|
8687
8977
|
async (request) => {
|
|
8688
8978
|
const { sessionId: rawId } = SessionIdParamSchema.parse(request.params);
|
|
8689
8979
|
const sessionId = decodeURIComponent(rawId);
|
|
8690
|
-
const
|
|
8691
|
-
if (!
|
|
8980
|
+
const isKnown = deps.core.sessionManager.getSession(sessionId) ?? deps.core.sessionManager.getSessionRecord(sessionId);
|
|
8981
|
+
if (!isKnown) {
|
|
8692
8982
|
throw new NotFoundError(
|
|
8693
8983
|
"SESSION_NOT_FOUND",
|
|
8694
8984
|
`Session "${sessionId}" not found`
|
|
@@ -8828,6 +9118,8 @@ var init_config_migrations = __esm({
|
|
|
8828
9118
|
log13 = createChildLogger({ module: "config-migrations" });
|
|
8829
9119
|
migrations = [
|
|
8830
9120
|
{
|
|
9121
|
+
// v2025.x: instanceName was added to support multi-instance setups.
|
|
9122
|
+
// Old configs lack this field — default to "Main" so the UI has a display name.
|
|
8831
9123
|
name: "add-instance-name",
|
|
8832
9124
|
apply(raw) {
|
|
8833
9125
|
if (raw.instanceName) return false;
|
|
@@ -8837,6 +9129,8 @@ var init_config_migrations = __esm({
|
|
|
8837
9129
|
}
|
|
8838
9130
|
},
|
|
8839
9131
|
{
|
|
9132
|
+
// displayVerbosity was replaced by outputMode — remove the legacy key
|
|
9133
|
+
// so it doesn't confuse Zod strict parsing or the config editor.
|
|
8840
9134
|
name: "delete-display-verbosity",
|
|
8841
9135
|
apply(raw) {
|
|
8842
9136
|
if (!("displayVerbosity" in raw)) return false;
|
|
@@ -8846,6 +9140,9 @@ var init_config_migrations = __esm({
|
|
|
8846
9140
|
}
|
|
8847
9141
|
},
|
|
8848
9142
|
{
|
|
9143
|
+
// Instance IDs were originally only in instances.json (the global registry).
|
|
9144
|
+
// This migration copies the ID into config.json so each instance is self-identifying
|
|
9145
|
+
// without needing to cross-reference the registry.
|
|
8849
9146
|
name: "add-instance-id",
|
|
8850
9147
|
apply(raw, ctx) {
|
|
8851
9148
|
if (raw.id) return false;
|
|
@@ -8906,10 +9203,11 @@ var init_config2 = __esm({
|
|
|
8906
9203
|
sessionLogRetentionDays: z4.number().default(30)
|
|
8907
9204
|
}).default({});
|
|
8908
9205
|
ConfigSchema = z4.object({
|
|
9206
|
+
/** Instance UUID, written once at creation time. */
|
|
8909
9207
|
id: z4.string().optional(),
|
|
8910
|
-
// instance UUID, written once at creation time
|
|
8911
9208
|
instanceName: z4.string().optional(),
|
|
8912
9209
|
defaultAgent: z4.string(),
|
|
9210
|
+
// --- Workspace security & path resolution ---
|
|
8913
9211
|
workspace: z4.object({
|
|
8914
9212
|
allowExternalWorkspaces: z4.boolean().default(true),
|
|
8915
9213
|
security: z4.object({
|
|
@@ -8917,12 +9215,16 @@ var init_config2 = __esm({
|
|
|
8917
9215
|
envWhitelist: z4.array(z4.string()).default([])
|
|
8918
9216
|
}).default({})
|
|
8919
9217
|
}).default({}),
|
|
9218
|
+
// --- Logging ---
|
|
8920
9219
|
logging: LoggingSchema,
|
|
9220
|
+
// --- Process lifecycle ---
|
|
8921
9221
|
runMode: z4.enum(["foreground", "daemon"]).default("foreground"),
|
|
8922
9222
|
autoStart: z4.boolean().default(false),
|
|
9223
|
+
// --- Session persistence ---
|
|
8923
9224
|
sessionStore: z4.object({
|
|
8924
9225
|
ttlDays: z4.number().default(30)
|
|
8925
9226
|
}).default({}),
|
|
9227
|
+
// --- Installed integration tracking (e.g. plugins installed via CLI) ---
|
|
8926
9228
|
integrations: z4.record(
|
|
8927
9229
|
z4.string(),
|
|
8928
9230
|
z4.object({
|
|
@@ -8930,7 +9232,9 @@ var init_config2 = __esm({
|
|
|
8930
9232
|
installedAt: z4.string().optional()
|
|
8931
9233
|
})
|
|
8932
9234
|
).default({}),
|
|
9235
|
+
// --- Agent output verbosity control ---
|
|
8933
9236
|
outputMode: z4.enum(["low", "medium", "high"]).default("medium").optional(),
|
|
9237
|
+
// --- Multi-agent switching behavior ---
|
|
8934
9238
|
agentSwitch: z4.object({
|
|
8935
9239
|
labelHistory: z4.boolean().default(true)
|
|
8936
9240
|
}).default({})
|
|
@@ -8946,6 +9250,13 @@ var init_config2 = __esm({
|
|
|
8946
9250
|
super();
|
|
8947
9251
|
this.configPath = process.env.OPENACP_CONFIG_PATH || configPath || expandHome2("~/.openacp/config.json");
|
|
8948
9252
|
}
|
|
9253
|
+
/**
|
|
9254
|
+
* Loads config from disk through the full validation pipeline:
|
|
9255
|
+
* 1. Create default config if missing (first run)
|
|
9256
|
+
* 2. Apply migrations for older config formats
|
|
9257
|
+
* 3. Apply environment variable overrides
|
|
9258
|
+
* 4. Validate against Zod schema — exits on failure
|
|
9259
|
+
*/
|
|
8949
9260
|
async load() {
|
|
8950
9261
|
const dir = path24.dirname(this.configPath);
|
|
8951
9262
|
fs21.mkdirSync(dir, { recursive: true });
|
|
@@ -8979,9 +9290,17 @@ var init_config2 = __esm({
|
|
|
8979
9290
|
}
|
|
8980
9291
|
this.config = result.data;
|
|
8981
9292
|
}
|
|
9293
|
+
/** Returns a deep clone of the current config to prevent external mutation. */
|
|
8982
9294
|
get() {
|
|
8983
9295
|
return structuredClone(this.config);
|
|
8984
9296
|
}
|
|
9297
|
+
/**
|
|
9298
|
+
* Merges partial updates into the config file using atomic write (write tmp + rename).
|
|
9299
|
+
*
|
|
9300
|
+
* Validates the merged result before writing. If `changePath` is provided,
|
|
9301
|
+
* emits a `config:changed` event with old and new values for that path,
|
|
9302
|
+
* enabling hot-reload without restart.
|
|
9303
|
+
*/
|
|
8985
9304
|
async save(updates, changePath) {
|
|
8986
9305
|
const oldConfig = this.config ? structuredClone(this.config) : void 0;
|
|
8987
9306
|
const raw = JSON.parse(fs21.readFileSync(this.configPath, "utf-8"));
|
|
@@ -9003,9 +9322,12 @@ var init_config2 = __esm({
|
|
|
9003
9322
|
}
|
|
9004
9323
|
}
|
|
9005
9324
|
/**
|
|
9006
|
-
*
|
|
9007
|
-
*
|
|
9008
|
-
*
|
|
9325
|
+
* Convenience wrapper for updating a single deeply-nested config field
|
|
9326
|
+
* without constructing the full update object manually.
|
|
9327
|
+
*
|
|
9328
|
+
* Accepts a dot-path (e.g. "logging.level") and builds the nested
|
|
9329
|
+
* update object internally before delegating to `save()`.
|
|
9330
|
+
* Throws if the path contains prototype-pollution keys.
|
|
9009
9331
|
*/
|
|
9010
9332
|
async setPath(dotPath, value) {
|
|
9011
9333
|
const BLOCKED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
@@ -9022,6 +9344,13 @@ var init_config2 = __esm({
|
|
|
9022
9344
|
target[parts[parts.length - 1]] = value;
|
|
9023
9345
|
await this.save(updates, dotPath);
|
|
9024
9346
|
}
|
|
9347
|
+
/**
|
|
9348
|
+
* Resolves a workspace path from user input.
|
|
9349
|
+
*
|
|
9350
|
+
* Supports three forms: no input (returns base dir), absolute/tilde paths
|
|
9351
|
+
* (validated against allowExternalWorkspaces), and named workspaces
|
|
9352
|
+
* (alphanumeric subdirectories under the base).
|
|
9353
|
+
*/
|
|
9025
9354
|
resolveWorkspace(input2) {
|
|
9026
9355
|
const workspaceBase = path24.dirname(path24.dirname(this.configPath));
|
|
9027
9356
|
if (!input2) {
|
|
@@ -9056,17 +9385,26 @@ var init_config2 = __esm({
|
|
|
9056
9385
|
fs21.mkdirSync(namedPath, { recursive: true });
|
|
9057
9386
|
return namedPath;
|
|
9058
9387
|
}
|
|
9388
|
+
/** 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
9389
|
async exists() {
|
|
9060
9390
|
return fs21.existsSync(this.configPath);
|
|
9061
9391
|
}
|
|
9392
|
+
/** Returns the resolved path to the config JSON file. */
|
|
9062
9393
|
getConfigPath() {
|
|
9063
9394
|
return this.configPath;
|
|
9064
9395
|
}
|
|
9396
|
+
/** Writes a complete config object to disk, creating the directory if needed. Used during initial setup. */
|
|
9065
9397
|
async writeNew(config) {
|
|
9066
9398
|
const dir = path24.dirname(this.configPath);
|
|
9067
9399
|
fs21.mkdirSync(dir, { recursive: true });
|
|
9068
9400
|
fs21.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
|
|
9069
9401
|
}
|
|
9402
|
+
/**
|
|
9403
|
+
* Applies `OPENACP_*` environment variables as overrides to per-plugin settings.
|
|
9404
|
+
*
|
|
9405
|
+
* This lets users configure plugin values (bot tokens, ports, etc.) via env vars
|
|
9406
|
+
* without editing settings files — useful for Docker, CI, and headless setups.
|
|
9407
|
+
*/
|
|
9070
9408
|
async applyEnvToPluginSettings(settingsManager) {
|
|
9071
9409
|
const pluginOverrides = [
|
|
9072
9410
|
{ envVar: "OPENACP_TUNNEL_ENABLED", pluginName: "@openacp/tunnel", key: "enabled", transform: (v) => v === "true" },
|
|
@@ -9093,6 +9431,7 @@ var init_config2 = __esm({
|
|
|
9093
9431
|
}
|
|
9094
9432
|
}
|
|
9095
9433
|
}
|
|
9434
|
+
/** Applies env var overrides to the raw config object before Zod validation. */
|
|
9096
9435
|
applyEnvOverrides(raw) {
|
|
9097
9436
|
const overrides = [
|
|
9098
9437
|
["OPENACP_DEFAULT_AGENT", ["defaultAgent"]],
|
|
@@ -9123,6 +9462,7 @@ var init_config2 = __esm({
|
|
|
9123
9462
|
raw.logging.level = "debug";
|
|
9124
9463
|
}
|
|
9125
9464
|
}
|
|
9465
|
+
/** Recursively merges source into target, skipping prototype-pollution keys. */
|
|
9126
9466
|
deepMerge(target, source) {
|
|
9127
9467
|
const DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
9128
9468
|
for (const key of Object.keys(source)) {
|
|
@@ -9933,6 +10273,7 @@ var init_instance_registry = __esm({
|
|
|
9933
10273
|
this.registryPath = registryPath;
|
|
9934
10274
|
}
|
|
9935
10275
|
data = { version: 1, instances: {} };
|
|
10276
|
+
/** Load the registry from disk. If the file is missing or corrupt, starts fresh. */
|
|
9936
10277
|
load() {
|
|
9937
10278
|
try {
|
|
9938
10279
|
const raw = fs22.readFileSync(this.registryPath, "utf-8");
|
|
@@ -9960,26 +10301,33 @@ var init_instance_registry = __esm({
|
|
|
9960
10301
|
this.save();
|
|
9961
10302
|
}
|
|
9962
10303
|
}
|
|
10304
|
+
/** Persist the registry to disk, creating parent directories if needed. */
|
|
9963
10305
|
save() {
|
|
9964
10306
|
const dir = path26.dirname(this.registryPath);
|
|
9965
10307
|
fs22.mkdirSync(dir, { recursive: true });
|
|
9966
10308
|
fs22.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
|
|
9967
10309
|
}
|
|
10310
|
+
/** Add or update an instance entry in the registry. Does not persist — call save() after. */
|
|
9968
10311
|
register(id, root) {
|
|
9969
10312
|
this.data.instances[id] = { id, root };
|
|
9970
10313
|
}
|
|
10314
|
+
/** Remove an instance entry. Does not persist — call save() after. */
|
|
9971
10315
|
remove(id) {
|
|
9972
10316
|
delete this.data.instances[id];
|
|
9973
10317
|
}
|
|
10318
|
+
/** Look up an instance by its ID. */
|
|
9974
10319
|
get(id) {
|
|
9975
10320
|
return this.data.instances[id];
|
|
9976
10321
|
}
|
|
10322
|
+
/** Look up an instance by its root directory path. */
|
|
9977
10323
|
getByRoot(root) {
|
|
9978
10324
|
return Object.values(this.data.instances).find((e) => e.root === root);
|
|
9979
10325
|
}
|
|
10326
|
+
/** Returns all registered instances. */
|
|
9980
10327
|
list() {
|
|
9981
10328
|
return Object.values(this.data.instances);
|
|
9982
10329
|
}
|
|
10330
|
+
/** Returns `baseId` if available, otherwise appends `-2`, `-3`, etc. until unique. */
|
|
9983
10331
|
uniqueId(baseId) {
|
|
9984
10332
|
if (!this.data.instances[baseId]) return baseId;
|
|
9985
10333
|
let n = 2;
|
|
@@ -10485,6 +10833,7 @@ var init_connection_manager = __esm({
|
|
|
10485
10833
|
"use strict";
|
|
10486
10834
|
ConnectionManager = class {
|
|
10487
10835
|
connections = /* @__PURE__ */ new Map();
|
|
10836
|
+
// Secondary index: sessionId → Set of connection IDs for O(1) broadcast targeting
|
|
10488
10837
|
sessionIndex = /* @__PURE__ */ new Map();
|
|
10489
10838
|
maxConnectionsPerSession;
|
|
10490
10839
|
maxTotalConnections;
|
|
@@ -10492,6 +10841,14 @@ var init_connection_manager = __esm({
|
|
|
10492
10841
|
this.maxConnectionsPerSession = opts?.maxPerSession ?? 10;
|
|
10493
10842
|
this.maxTotalConnections = opts?.maxTotal ?? 100;
|
|
10494
10843
|
}
|
|
10844
|
+
/**
|
|
10845
|
+
* Registers a new SSE connection for the given session.
|
|
10846
|
+
*
|
|
10847
|
+
* Wires a `close` listener on the response so the connection is automatically
|
|
10848
|
+
* removed when the client disconnects (browser tab closed, network drop, etc.).
|
|
10849
|
+
*
|
|
10850
|
+
* @throws if the global or per-session connection limit is reached.
|
|
10851
|
+
*/
|
|
10495
10852
|
addConnection(sessionId, tokenId, response) {
|
|
10496
10853
|
if (this.connections.size >= this.maxTotalConnections) {
|
|
10497
10854
|
throw new Error("Maximum total connections reached");
|
|
@@ -10512,6 +10869,7 @@ var init_connection_manager = __esm({
|
|
|
10512
10869
|
response.on("close", () => this.removeConnection(id));
|
|
10513
10870
|
return connection;
|
|
10514
10871
|
}
|
|
10872
|
+
/** Remove a connection from both indexes. Called automatically on client disconnect. */
|
|
10515
10873
|
removeConnection(connectionId) {
|
|
10516
10874
|
const conn = this.connections.get(connectionId);
|
|
10517
10875
|
if (!conn) return;
|
|
@@ -10522,11 +10880,20 @@ var init_connection_manager = __esm({
|
|
|
10522
10880
|
if (sessionConns.size === 0) this.sessionIndex.delete(conn.sessionId);
|
|
10523
10881
|
}
|
|
10524
10882
|
}
|
|
10883
|
+
/** Returns all active connections for a session. */
|
|
10525
10884
|
getConnectionsBySession(sessionId) {
|
|
10526
10885
|
const connIds = this.sessionIndex.get(sessionId);
|
|
10527
10886
|
if (!connIds) return [];
|
|
10528
10887
|
return Array.from(connIds).map((id) => this.connections.get(id)).filter((c3) => c3 !== void 0);
|
|
10529
10888
|
}
|
|
10889
|
+
/**
|
|
10890
|
+
* Writes a serialized SSE event to all connections for the given session.
|
|
10891
|
+
*
|
|
10892
|
+
* Backpressure handling: if `response.write()` returns false (OS send buffer full),
|
|
10893
|
+
* the connection is flagged as `backpressured`. On the next write attempt, if it is
|
|
10894
|
+
* still backpressured, the connection is forcibly closed to prevent unbounded memory
|
|
10895
|
+
* growth from queuing writes on a slow or stalled client.
|
|
10896
|
+
*/
|
|
10530
10897
|
broadcast(sessionId, serializedEvent) {
|
|
10531
10898
|
for (const conn of this.getConnectionsBySession(sessionId)) {
|
|
10532
10899
|
if (conn.response.writableEnded) continue;
|
|
@@ -10548,6 +10915,10 @@ var init_connection_manager = __esm({
|
|
|
10548
10915
|
}
|
|
10549
10916
|
}
|
|
10550
10917
|
}
|
|
10918
|
+
/**
|
|
10919
|
+
* Force-close all connections associated with a given auth token.
|
|
10920
|
+
* Called when a token is revoked to immediately terminate those streams.
|
|
10921
|
+
*/
|
|
10551
10922
|
disconnectByToken(tokenId) {
|
|
10552
10923
|
for (const [id, conn] of this.connections) {
|
|
10553
10924
|
if (conn.tokenId === tokenId) {
|
|
@@ -10556,9 +10927,11 @@ var init_connection_manager = __esm({
|
|
|
10556
10927
|
}
|
|
10557
10928
|
}
|
|
10558
10929
|
}
|
|
10930
|
+
/** Returns a snapshot of all active connections (used by the admin endpoint). */
|
|
10559
10931
|
listConnections() {
|
|
10560
10932
|
return Array.from(this.connections.values());
|
|
10561
10933
|
}
|
|
10934
|
+
/** Close all connections and clear all indexes. Called on plugin teardown. */
|
|
10562
10935
|
cleanup() {
|
|
10563
10936
|
for (const [, conn] of this.connections) {
|
|
10564
10937
|
if (!conn.response.writableEnded) conn.response.end();
|
|
@@ -10576,10 +10949,15 @@ var init_event_buffer = __esm({
|
|
|
10576
10949
|
"src/plugins/sse-adapter/event-buffer.ts"() {
|
|
10577
10950
|
"use strict";
|
|
10578
10951
|
EventBuffer = class {
|
|
10952
|
+
/**
|
|
10953
|
+
* @param maxSize Maximum events retained per session. Older events are evicted when
|
|
10954
|
+
* this limit is exceeded. Defaults to 100.
|
|
10955
|
+
*/
|
|
10579
10956
|
constructor(maxSize = 100) {
|
|
10580
10957
|
this.maxSize = maxSize;
|
|
10581
10958
|
}
|
|
10582
10959
|
buffers = /* @__PURE__ */ new Map();
|
|
10960
|
+
/** Append an event to the session's buffer, evicting the oldest entry if at capacity. */
|
|
10583
10961
|
push(sessionId, event) {
|
|
10584
10962
|
let buffer = this.buffers.get(sessionId);
|
|
10585
10963
|
if (!buffer) {
|
|
@@ -10591,6 +10969,14 @@ var init_event_buffer = __esm({
|
|
|
10591
10969
|
buffer.shift();
|
|
10592
10970
|
}
|
|
10593
10971
|
}
|
|
10972
|
+
/**
|
|
10973
|
+
* Returns events that occurred after `lastEventId`.
|
|
10974
|
+
*
|
|
10975
|
+
* - If `lastEventId` is `undefined`, returns all buffered events (fresh connection).
|
|
10976
|
+
* - If `lastEventId` is not found in the buffer, returns `null` — the event has been
|
|
10977
|
+
* evicted and the client must be informed that a gap may exist.
|
|
10978
|
+
* - Otherwise returns the slice after the matching event.
|
|
10979
|
+
*/
|
|
10594
10980
|
getSince(sessionId, lastEventId) {
|
|
10595
10981
|
const buffer = this.buffers.get(sessionId);
|
|
10596
10982
|
if (!buffer || buffer.length === 0) return [];
|
|
@@ -10599,6 +10985,7 @@ var init_event_buffer = __esm({
|
|
|
10599
10985
|
if (index === -1) return null;
|
|
10600
10986
|
return buffer.slice(index + 1);
|
|
10601
10987
|
}
|
|
10988
|
+
/** Remove the buffer for a session — called when the session ends to free memory. */
|
|
10602
10989
|
cleanup(sessionId) {
|
|
10603
10990
|
this.buffers.delete(sessionId);
|
|
10604
10991
|
}
|
|
@@ -10680,6 +11067,12 @@ var init_adapter = __esm({
|
|
|
10680
11067
|
voice: false
|
|
10681
11068
|
};
|
|
10682
11069
|
heartbeatTimer;
|
|
11070
|
+
/**
|
|
11071
|
+
* Starts the heartbeat timer that keeps idle SSE connections alive.
|
|
11072
|
+
*
|
|
11073
|
+
* `.unref()` prevents the timer from blocking the Node.js event loop from
|
|
11074
|
+
* exiting if this is the only remaining async operation (e.g. during tests).
|
|
11075
|
+
*/
|
|
10683
11076
|
async start() {
|
|
10684
11077
|
this.heartbeatTimer = setInterval(() => {
|
|
10685
11078
|
const heartbeat = serializeHeartbeat();
|
|
@@ -10696,6 +11089,7 @@ var init_adapter = __esm({
|
|
|
10696
11089
|
this.heartbeatTimer.unref();
|
|
10697
11090
|
}
|
|
10698
11091
|
}
|
|
11092
|
+
/** Stops the heartbeat timer and closes all active connections. */
|
|
10699
11093
|
async stop() {
|
|
10700
11094
|
if (this.heartbeatTimer) {
|
|
10701
11095
|
clearInterval(this.heartbeatTimer);
|
|
@@ -10703,18 +11097,32 @@ var init_adapter = __esm({
|
|
|
10703
11097
|
}
|
|
10704
11098
|
this.connectionManager.cleanup();
|
|
10705
11099
|
}
|
|
11100
|
+
/**
|
|
11101
|
+
* Serializes an outgoing agent message, pushes it to the event buffer,
|
|
11102
|
+
* then broadcasts it to all active connections for the session.
|
|
11103
|
+
*
|
|
11104
|
+
* Buffering before broadcast ensures that a client reconnecting immediately
|
|
11105
|
+
* after this call can still replay the event via `Last-Event-ID`.
|
|
11106
|
+
*/
|
|
10706
11107
|
async sendMessage(sessionId, content) {
|
|
10707
11108
|
const eventId = generateEventId();
|
|
10708
11109
|
const serialized = serializeOutgoingMessage(sessionId, eventId, content);
|
|
10709
11110
|
this.eventBuffer.push(sessionId, { id: eventId, data: serialized });
|
|
10710
11111
|
this.connectionManager.broadcast(sessionId, serialized);
|
|
10711
11112
|
}
|
|
11113
|
+
/** Serializes and delivers a permission request UI to the session's SSE clients. */
|
|
10712
11114
|
async sendPermissionRequest(sessionId, request) {
|
|
10713
11115
|
const eventId = generateEventId();
|
|
10714
11116
|
const serialized = serializePermissionRequest(sessionId, eventId, request);
|
|
10715
11117
|
this.eventBuffer.push(sessionId, { id: eventId, data: serialized });
|
|
10716
11118
|
this.connectionManager.broadcast(sessionId, serialized);
|
|
10717
11119
|
}
|
|
11120
|
+
/**
|
|
11121
|
+
* Delivers a cross-session notification to the target session's SSE clients.
|
|
11122
|
+
*
|
|
11123
|
+
* Notifications are always buffered in addition to being broadcast so that
|
|
11124
|
+
* a client reconnecting shortly after (e.g. page refresh) still sees the alert.
|
|
11125
|
+
*/
|
|
10718
11126
|
async sendNotification(notification) {
|
|
10719
11127
|
if (notification.sessionId) {
|
|
10720
11128
|
const eventId = generateEventId();
|
|
@@ -10723,9 +11131,11 @@ var init_adapter = __esm({
|
|
|
10723
11131
|
this.connectionManager.broadcast(notification.sessionId, serialized);
|
|
10724
11132
|
}
|
|
10725
11133
|
}
|
|
11134
|
+
/** SSE has no concept of threads — return sessionId as the threadId */
|
|
10726
11135
|
async createSessionThread(sessionId, _name) {
|
|
10727
11136
|
return sessionId;
|
|
10728
11137
|
}
|
|
11138
|
+
/** No-op for SSE — there are no named threads to rename. */
|
|
10729
11139
|
async renameSessionThread(_sessionId, _newName) {
|
|
10730
11140
|
}
|
|
10731
11141
|
};
|
|
@@ -10765,6 +11175,7 @@ async function sseRoutes(app, deps) {
|
|
|
10765
11175
|
"Content-Type": "text/event-stream",
|
|
10766
11176
|
"Cache-Control": "no-cache",
|
|
10767
11177
|
"Connection": "keep-alive",
|
|
11178
|
+
// Disable buffering in Nginx/Cloudflare so events arrive without delay
|
|
10768
11179
|
"X-Accel-Buffering": "no"
|
|
10769
11180
|
});
|
|
10770
11181
|
raw.write(serializeConnected(connection.id, sessionId));
|
|
@@ -13694,13 +14105,16 @@ var init_settings_manager = __esm({
|
|
|
13694
14105
|
constructor(basePath) {
|
|
13695
14106
|
this.basePath = basePath;
|
|
13696
14107
|
}
|
|
14108
|
+
/** Returns the base path for all plugin settings directories. */
|
|
13697
14109
|
getBasePath() {
|
|
13698
14110
|
return this.basePath;
|
|
13699
14111
|
}
|
|
14112
|
+
/** Create a SettingsAPI instance scoped to a specific plugin. */
|
|
13700
14113
|
createAPI(pluginName) {
|
|
13701
14114
|
const settingsPath = this.getSettingsPath(pluginName);
|
|
13702
14115
|
return new SettingsAPIImpl(settingsPath);
|
|
13703
14116
|
}
|
|
14117
|
+
/** Load a plugin's settings from disk. Returns empty object if file doesn't exist. */
|
|
13704
14118
|
async loadSettings(pluginName) {
|
|
13705
14119
|
const settingsPath = this.getSettingsPath(pluginName);
|
|
13706
14120
|
try {
|
|
@@ -13710,6 +14124,7 @@ var init_settings_manager = __esm({
|
|
|
13710
14124
|
return {};
|
|
13711
14125
|
}
|
|
13712
14126
|
}
|
|
14127
|
+
/** Validate settings against a Zod schema. Returns valid if no schema is provided. */
|
|
13713
14128
|
validateSettings(_pluginName, settings, schema) {
|
|
13714
14129
|
if (!schema) return { valid: true };
|
|
13715
14130
|
const result = schema.safeParse(settings);
|
|
@@ -13721,12 +14136,14 @@ var init_settings_manager = __esm({
|
|
|
13721
14136
|
)
|
|
13722
14137
|
};
|
|
13723
14138
|
}
|
|
14139
|
+
/** Resolve the absolute path to a plugin's settings.json file. */
|
|
13724
14140
|
getSettingsPath(pluginName) {
|
|
13725
14141
|
return path30.join(this.basePath, pluginName, "settings.json");
|
|
13726
14142
|
}
|
|
13727
14143
|
async getPluginSettings(pluginName) {
|
|
13728
14144
|
return this.loadSettings(pluginName);
|
|
13729
14145
|
}
|
|
14146
|
+
/** Merge updates into existing settings (shallow merge). */
|
|
13730
14147
|
async updatePluginSettings(pluginName, updates) {
|
|
13731
14148
|
const api = this.createAPI(pluginName);
|
|
13732
14149
|
const current = await api.getAll();
|
|
@@ -14297,6 +14714,12 @@ var init_doctor = __esm({
|
|
|
14297
14714
|
this.dryRun = options?.dryRun ?? false;
|
|
14298
14715
|
this.dataDir = options.dataDir;
|
|
14299
14716
|
}
|
|
14717
|
+
/**
|
|
14718
|
+
* Executes all checks and returns an aggregated report.
|
|
14719
|
+
*
|
|
14720
|
+
* Safe fixes are applied inline (mutating CheckResult.message to show "Fixed").
|
|
14721
|
+
* Risky fixes are deferred to `report.pendingFixes` for user confirmation.
|
|
14722
|
+
*/
|
|
14300
14723
|
async runAll() {
|
|
14301
14724
|
const ctx = await this.buildContext();
|
|
14302
14725
|
const checks = [...ALL_CHECKS].sort((a, b) => a.order - b.order);
|
|
@@ -14344,6 +14767,7 @@ var init_doctor = __esm({
|
|
|
14344
14767
|
}
|
|
14345
14768
|
return { categories, summary, pendingFixes };
|
|
14346
14769
|
}
|
|
14770
|
+
/** Constructs the shared context used by all checks — loads config if available. */
|
|
14347
14771
|
async buildContext() {
|
|
14348
14772
|
const dataDir = this.dataDir;
|
|
14349
14773
|
const configPath = process.env.OPENACP_CONFIG_PATH || path35.join(dataDir, "config.json");
|
|
@@ -14991,6 +15415,14 @@ var init_permissions = __esm({
|
|
|
14991
15415
|
this.sendNotification = sendNotification;
|
|
14992
15416
|
}
|
|
14993
15417
|
pending = /* @__PURE__ */ new Map();
|
|
15418
|
+
/**
|
|
15419
|
+
* Send a permission request to the session's topic as an inline keyboard message,
|
|
15420
|
+
* and fire a notification to the Notifications topic so the user is alerted.
|
|
15421
|
+
*
|
|
15422
|
+
* Each button encodes `p:<callbackKey>:<optionId>`. The callbackKey is a short
|
|
15423
|
+
* nanoid that maps to the full pending state stored in-memory, avoiding the
|
|
15424
|
+
* 64-byte Telegram callback_data limit.
|
|
15425
|
+
*/
|
|
14994
15426
|
async sendPermissionRequest(session, request) {
|
|
14995
15427
|
const threadId = Number(session.threadId);
|
|
14996
15428
|
const callbackKey = nanoid3(8);
|
|
@@ -15025,6 +15457,12 @@ ${escapeHtml4(request.description)}`,
|
|
|
15025
15457
|
deepLink
|
|
15026
15458
|
});
|
|
15027
15459
|
}
|
|
15460
|
+
/**
|
|
15461
|
+
* Register the `p:` callback handler in the bot's middleware chain.
|
|
15462
|
+
*
|
|
15463
|
+
* Must be called during setup so grammY processes permission responses
|
|
15464
|
+
* before other generic callback handlers.
|
|
15465
|
+
*/
|
|
15028
15466
|
setupCallbackHandler() {
|
|
15029
15467
|
this.bot.on("callback_query:data", async (ctx, next) => {
|
|
15030
15468
|
const data = ctx.callbackQuery.data;
|
|
@@ -15072,6 +15510,9 @@ var init_tool_card_state = __esm({
|
|
|
15072
15510
|
specs = [];
|
|
15073
15511
|
planEntries;
|
|
15074
15512
|
usage;
|
|
15513
|
+
// Lifecycle: active (first flush pending) → active (subsequent updates debounced) → finalized.
|
|
15514
|
+
// Once finalized, all updateFromSpec/updatePlan/appendUsage/finalize() calls are no-ops —
|
|
15515
|
+
// guards against events arriving after the session has ended or the tool has already completed.
|
|
15075
15516
|
finalized = false;
|
|
15076
15517
|
isFirstFlush = true;
|
|
15077
15518
|
debounceTimer;
|
|
@@ -15079,6 +15520,7 @@ var init_tool_card_state = __esm({
|
|
|
15079
15520
|
constructor(config) {
|
|
15080
15521
|
this.onFlush = config.onFlush;
|
|
15081
15522
|
}
|
|
15523
|
+
/** Adds or updates a tool spec. First call flushes immediately; subsequent calls are debounced. */
|
|
15082
15524
|
updateFromSpec(spec) {
|
|
15083
15525
|
if (this.finalized) return;
|
|
15084
15526
|
const existingIdx = this.specs.findIndex((s) => s.id === spec.id);
|
|
@@ -15094,6 +15536,7 @@ var init_tool_card_state = __esm({
|
|
|
15094
15536
|
this.scheduleFlush();
|
|
15095
15537
|
}
|
|
15096
15538
|
}
|
|
15539
|
+
/** Updates the plan entries displayed alongside tool cards. */
|
|
15097
15540
|
updatePlan(entries) {
|
|
15098
15541
|
if (this.finalized) return;
|
|
15099
15542
|
this.planEntries = entries;
|
|
@@ -15104,17 +15547,20 @@ var init_tool_card_state = __esm({
|
|
|
15104
15547
|
this.scheduleFlush();
|
|
15105
15548
|
}
|
|
15106
15549
|
}
|
|
15550
|
+
/** Appends token usage data to the tool card (typically at end of turn). */
|
|
15107
15551
|
appendUsage(usage) {
|
|
15108
15552
|
if (this.finalized) return;
|
|
15109
15553
|
this.usage = usage;
|
|
15110
15554
|
this.scheduleFlush();
|
|
15111
15555
|
}
|
|
15556
|
+
/** Marks the turn as complete and flushes the final snapshot immediately. */
|
|
15112
15557
|
finalize() {
|
|
15113
15558
|
if (this.finalized) return;
|
|
15114
15559
|
this.finalized = true;
|
|
15115
15560
|
this.clearDebounce();
|
|
15116
15561
|
this.flush();
|
|
15117
15562
|
}
|
|
15563
|
+
/** Stops all pending flushes without emitting a final snapshot. */
|
|
15118
15564
|
destroy() {
|
|
15119
15565
|
this.finalized = true;
|
|
15120
15566
|
this.clearDebounce();
|
|
@@ -15220,9 +15666,11 @@ var init_stream_accumulator = __esm({
|
|
|
15220
15666
|
entry.diffStats = update.diffStats;
|
|
15221
15667
|
}
|
|
15222
15668
|
}
|
|
15669
|
+
/** Retrieves a tool entry by ID, or undefined if not yet tracked. */
|
|
15223
15670
|
get(id) {
|
|
15224
15671
|
return this.entries.get(id);
|
|
15225
15672
|
}
|
|
15673
|
+
/** Resets all state between turns. */
|
|
15226
15674
|
clear() {
|
|
15227
15675
|
this.entries.clear();
|
|
15228
15676
|
this.pendingUpdates.clear();
|
|
@@ -15231,20 +15679,24 @@ var init_stream_accumulator = __esm({
|
|
|
15231
15679
|
ThoughtBuffer = class {
|
|
15232
15680
|
chunks = [];
|
|
15233
15681
|
sealed = false;
|
|
15682
|
+
/** Appends a thought text chunk. Ignored if already sealed. */
|
|
15234
15683
|
append(chunk) {
|
|
15235
15684
|
if (this.sealed) return;
|
|
15236
15685
|
this.chunks.push(chunk);
|
|
15237
15686
|
}
|
|
15687
|
+
/** Marks the thought as complete and returns the full accumulated text. */
|
|
15238
15688
|
seal() {
|
|
15239
15689
|
this.sealed = true;
|
|
15240
15690
|
return this.chunks.join("");
|
|
15241
15691
|
}
|
|
15692
|
+
/** Returns the text accumulated so far without sealing. */
|
|
15242
15693
|
getText() {
|
|
15243
15694
|
return this.chunks.join("");
|
|
15244
15695
|
}
|
|
15245
15696
|
isSealed() {
|
|
15246
15697
|
return this.sealed;
|
|
15247
15698
|
}
|
|
15699
|
+
/** Resets the buffer for reuse in a new turn. */
|
|
15248
15700
|
reset() {
|
|
15249
15701
|
this.chunks = [];
|
|
15250
15702
|
this.sealed = false;
|
|
@@ -15416,6 +15868,13 @@ var init_display_spec_builder = __esm({
|
|
|
15416
15868
|
constructor(tunnelService) {
|
|
15417
15869
|
this.tunnelService = tunnelService;
|
|
15418
15870
|
}
|
|
15871
|
+
/**
|
|
15872
|
+
* Builds a display spec for a single tool call entry.
|
|
15873
|
+
*
|
|
15874
|
+
* Deduplicates fields to avoid repeating the same info (e.g., if the title
|
|
15875
|
+
* was derived from the command, the command field is omitted). For long
|
|
15876
|
+
* output, generates a viewer link via the tunnel service when available.
|
|
15877
|
+
*/
|
|
15419
15878
|
buildToolSpec(entry, mode, sessionContext) {
|
|
15420
15879
|
const effectiveKind = entry.displayKind ?? (isApplyPatchOtherTool(entry.kind, entry.name, entry.rawInput) ? "edit" : entry.kind);
|
|
15421
15880
|
const icon = KIND_ICONS[effectiveKind] ?? KIND_ICONS["other"] ?? "\u{1F6E0}\uFE0F";
|
|
@@ -15474,6 +15933,7 @@ var init_display_spec_builder = __esm({
|
|
|
15474
15933
|
isHidden
|
|
15475
15934
|
};
|
|
15476
15935
|
}
|
|
15936
|
+
/** Builds a display spec for an agent thought. Content is only included at high verbosity. */
|
|
15477
15937
|
buildThoughtSpec(content, mode) {
|
|
15478
15938
|
const indicator = "Thinking...";
|
|
15479
15939
|
return {
|
|
@@ -15607,7 +16067,10 @@ var init_activity = __esm({
|
|
|
15607
16067
|
this.sessionId = sessionId;
|
|
15608
16068
|
this.state = new ToolCardState({
|
|
15609
16069
|
onFlush: (snapshot) => {
|
|
15610
|
-
this.flushPromise = this.flushPromise.then(() =>
|
|
16070
|
+
this.flushPromise = this.flushPromise.then(() => {
|
|
16071
|
+
if (this.aborted) return;
|
|
16072
|
+
return this._sendOrEdit(snapshot);
|
|
16073
|
+
}).catch(() => {
|
|
15611
16074
|
});
|
|
15612
16075
|
}
|
|
15613
16076
|
});
|
|
@@ -15617,6 +16080,7 @@ var init_activity = __esm({
|
|
|
15617
16080
|
lastSentText;
|
|
15618
16081
|
flushPromise = Promise.resolve();
|
|
15619
16082
|
overflowMsgIds = [];
|
|
16083
|
+
aborted = false;
|
|
15620
16084
|
tracer;
|
|
15621
16085
|
sessionId;
|
|
15622
16086
|
updateFromSpec(spec) {
|
|
@@ -15633,6 +16097,7 @@ var init_activity = __esm({
|
|
|
15633
16097
|
await this.flushPromise;
|
|
15634
16098
|
}
|
|
15635
16099
|
destroy() {
|
|
16100
|
+
this.aborted = true;
|
|
15636
16101
|
this.state.destroy();
|
|
15637
16102
|
}
|
|
15638
16103
|
hasContent() {
|
|
@@ -15864,6 +16329,13 @@ var init_send_queue = __esm({
|
|
|
15864
16329
|
get pending() {
|
|
15865
16330
|
return this.items.length;
|
|
15866
16331
|
}
|
|
16332
|
+
/**
|
|
16333
|
+
* Queues an async operation for rate-limited execution.
|
|
16334
|
+
*
|
|
16335
|
+
* For text-type items with a key, replaces any existing queued item with
|
|
16336
|
+
* the same key (deduplication). This is used for streaming draft updates
|
|
16337
|
+
* where only the latest content matters.
|
|
16338
|
+
*/
|
|
15867
16339
|
enqueue(fn, opts) {
|
|
15868
16340
|
const type = opts?.type ?? "other";
|
|
15869
16341
|
const key = opts?.key;
|
|
@@ -15891,6 +16363,11 @@ var init_send_queue = __esm({
|
|
|
15891
16363
|
this.scheduleProcess();
|
|
15892
16364
|
return promise;
|
|
15893
16365
|
}
|
|
16366
|
+
/**
|
|
16367
|
+
* Called when a platform rate limit is hit. Drops all pending text items
|
|
16368
|
+
* (draft updates) to reduce backlog, keeping only non-text items that
|
|
16369
|
+
* represent important operations (e.g., permission requests).
|
|
16370
|
+
*/
|
|
15894
16371
|
onRateLimited() {
|
|
15895
16372
|
this.config.onRateLimited?.();
|
|
15896
16373
|
const remaining2 = [];
|
|
@@ -15909,6 +16386,10 @@ var init_send_queue = __esm({
|
|
|
15909
16386
|
}
|
|
15910
16387
|
this.items = [];
|
|
15911
16388
|
}
|
|
16389
|
+
/**
|
|
16390
|
+
* Schedules the next item for processing after the rate-limit delay.
|
|
16391
|
+
* Uses per-category timing when available, falling back to the global minInterval.
|
|
16392
|
+
*/
|
|
15912
16393
|
scheduleProcess() {
|
|
15913
16394
|
if (this.processing) return;
|
|
15914
16395
|
if (this.items.length === 0) return;
|
|
@@ -15981,6 +16462,7 @@ var init_streaming = __esm({
|
|
|
15981
16462
|
lastSentHtml = "";
|
|
15982
16463
|
displayTruncated = false;
|
|
15983
16464
|
tracer;
|
|
16465
|
+
/** Append a text chunk to the buffer and schedule a throttled flush. */
|
|
15984
16466
|
append(text5) {
|
|
15985
16467
|
if (!text5) return;
|
|
15986
16468
|
this.buffer += text5;
|
|
@@ -16060,6 +16542,16 @@ var init_streaming = __esm({
|
|
|
16060
16542
|
}
|
|
16061
16543
|
}
|
|
16062
16544
|
}
|
|
16545
|
+
/**
|
|
16546
|
+
* Flush the complete buffer as the final message for this turn.
|
|
16547
|
+
*
|
|
16548
|
+
* Cancels any pending debounce timer, waits for in-flight flushes to settle,
|
|
16549
|
+
* then sends the full content. If the HTML exceeds 4096 bytes, the content is
|
|
16550
|
+
* split at markdown boundaries to avoid breaking HTML tags mid-tag.
|
|
16551
|
+
*
|
|
16552
|
+
* Returns the Telegram message ID of the sent/edited message, or undefined
|
|
16553
|
+
* if nothing was sent (empty buffer or all network calls failed).
|
|
16554
|
+
*/
|
|
16063
16555
|
async finalize() {
|
|
16064
16556
|
this.tracer?.log("telegram", { action: "draft:finalize", sessionId: this.sessionId, bufferLen: this.buffer.length, msgId: this.messageId });
|
|
16065
16557
|
if (this.flushTimer) {
|
|
@@ -16131,9 +16623,14 @@ var init_streaming = __esm({
|
|
|
16131
16623
|
this.tracer?.log("telegram", { action: "draft:finalize:split", sessionId: this.sessionId, chunks: mdChunks.length });
|
|
16132
16624
|
return this.messageId;
|
|
16133
16625
|
}
|
|
16626
|
+
/** Returns the Telegram message ID for this draft, or undefined if not yet sent. */
|
|
16134
16627
|
getMessageId() {
|
|
16135
16628
|
return this.messageId;
|
|
16136
16629
|
}
|
|
16630
|
+
/**
|
|
16631
|
+
* Strip occurrences of `pattern` from the buffer and edit the message in-place.
|
|
16632
|
+
* Used by the TTS plugin to remove [TTS]...[/TTS] blocks after audio is sent.
|
|
16633
|
+
*/
|
|
16137
16634
|
async stripPattern(pattern) {
|
|
16138
16635
|
if (!this.messageId || !this.buffer) return;
|
|
16139
16636
|
const stripped = this.buffer.replace(pattern, "").trim();
|
|
@@ -16171,6 +16668,10 @@ var init_draft_manager = __esm({
|
|
|
16171
16668
|
drafts = /* @__PURE__ */ new Map();
|
|
16172
16669
|
textBuffers = /* @__PURE__ */ new Map();
|
|
16173
16670
|
finalizedDrafts = /* @__PURE__ */ new Map();
|
|
16671
|
+
/**
|
|
16672
|
+
* Return the active draft for a session, creating one if it doesn't exist yet.
|
|
16673
|
+
* Only one draft per session exists at a time.
|
|
16674
|
+
*/
|
|
16174
16675
|
getOrCreate(sessionId, threadId, tracer = null) {
|
|
16175
16676
|
let draft = this.drafts.get(sessionId);
|
|
16176
16677
|
if (!draft) {
|
|
@@ -16199,7 +16700,12 @@ var init_draft_manager = __esm({
|
|
|
16199
16700
|
);
|
|
16200
16701
|
}
|
|
16201
16702
|
/**
|
|
16202
|
-
* Finalize the
|
|
16703
|
+
* Finalize the active draft for a session and retain a short-lived reference for post-send edits.
|
|
16704
|
+
*
|
|
16705
|
+
* Removes the draft from the active map before awaiting to prevent concurrent calls from
|
|
16706
|
+
* double-finalizing the same draft. If the draft produces a message ID, stores it as a
|
|
16707
|
+
* `FinalizedDraft` so `stripPattern` (e.g. TTS block removal) can still edit the message
|
|
16708
|
+
* after it has been sent.
|
|
16203
16709
|
*/
|
|
16204
16710
|
async finalize(sessionId, _assistantSessionId) {
|
|
16205
16711
|
const draft = this.drafts.get(sessionId);
|
|
@@ -16226,6 +16732,12 @@ var init_draft_manager = __esm({
|
|
|
16226
16732
|
await finalized.draft.stripPattern(pattern);
|
|
16227
16733
|
}
|
|
16228
16734
|
}
|
|
16735
|
+
/**
|
|
16736
|
+
* Discard all draft state for a session without sending anything.
|
|
16737
|
+
*
|
|
16738
|
+
* Removes the active draft, text buffer, and finalized draft reference. Called when a
|
|
16739
|
+
* session ends or is reset and any unsent content should be silently dropped.
|
|
16740
|
+
*/
|
|
16229
16741
|
cleanup(sessionId) {
|
|
16230
16742
|
this.drafts.delete(sessionId);
|
|
16231
16743
|
this.textBuffers.delete(sessionId);
|
|
@@ -16244,14 +16756,19 @@ var init_skill_command_manager = __esm({
|
|
|
16244
16756
|
init_log();
|
|
16245
16757
|
log28 = createChildLogger({ module: "skill-commands" });
|
|
16246
16758
|
SkillCommandManager = class {
|
|
16247
|
-
// sessionId → pinned msgId
|
|
16248
16759
|
constructor(bot, chatId, sendQueue, sessionManager) {
|
|
16249
16760
|
this.bot = bot;
|
|
16250
16761
|
this.chatId = chatId;
|
|
16251
16762
|
this.sendQueue = sendQueue;
|
|
16252
16763
|
this.sessionManager = sessionManager;
|
|
16253
16764
|
}
|
|
16765
|
+
// sessionId → Telegram message ID of the pinned skills message
|
|
16254
16766
|
messages = /* @__PURE__ */ new Map();
|
|
16767
|
+
/**
|
|
16768
|
+
* Send or update the pinned skill commands message for a session.
|
|
16769
|
+
* Creates a new pinned message if none exists; edits the existing one otherwise.
|
|
16770
|
+
* Passing an empty `commands` array removes the pinned message.
|
|
16771
|
+
*/
|
|
16255
16772
|
async send(sessionId, threadId, commands) {
|
|
16256
16773
|
if (!this.messages.has(sessionId)) {
|
|
16257
16774
|
const record = this.sessionManager.getSessionRecord(sessionId);
|
|
@@ -16351,11 +16868,21 @@ var init_messaging_adapter = __esm({
|
|
|
16351
16868
|
this.adapterConfig = adapterConfig;
|
|
16352
16869
|
}
|
|
16353
16870
|
// === Message dispatch flow ===
|
|
16871
|
+
/**
|
|
16872
|
+
* Entry point for all outbound messages from sessions to the platform.
|
|
16873
|
+
* Resolves the current verbosity, filters messages that should be hidden,
|
|
16874
|
+
* then dispatches to the appropriate type-specific handler.
|
|
16875
|
+
*/
|
|
16354
16876
|
async sendMessage(sessionId, content) {
|
|
16355
16877
|
const verbosity = this.getVerbosity();
|
|
16356
16878
|
if (!this.shouldDisplay(content, verbosity)) return;
|
|
16357
16879
|
await this.dispatchMessage(sessionId, content, verbosity);
|
|
16358
16880
|
}
|
|
16881
|
+
/**
|
|
16882
|
+
* Routes a message to its type-specific handler.
|
|
16883
|
+
* Subclasses can override this for custom dispatch logic, but typically
|
|
16884
|
+
* override individual handle* methods instead.
|
|
16885
|
+
*/
|
|
16359
16886
|
async dispatchMessage(sessionId, content, verbosity) {
|
|
16360
16887
|
switch (content.type) {
|
|
16361
16888
|
case "text":
|
|
@@ -16393,6 +16920,8 @@ var init_messaging_adapter = __esm({
|
|
|
16393
16920
|
}
|
|
16394
16921
|
}
|
|
16395
16922
|
// === Default handlers — all protected, all overridable ===
|
|
16923
|
+
// Each handler is a no-op by default. Subclasses override only the message
|
|
16924
|
+
// types they support (e.g., Telegram overrides handleText, handleToolCall, etc.).
|
|
16396
16925
|
async handleText(_sessionId, _content) {
|
|
16397
16926
|
}
|
|
16398
16927
|
async handleThought(_sessionId, _content, _verbosity) {
|
|
@@ -16426,6 +16955,10 @@ var init_messaging_adapter = __esm({
|
|
|
16426
16955
|
async handleResourceLink(_sessionId, _content) {
|
|
16427
16956
|
}
|
|
16428
16957
|
// === Helpers ===
|
|
16958
|
+
/**
|
|
16959
|
+
* Resolves the current output verbosity by checking (in priority order):
|
|
16960
|
+
* per-channel config, global config, then adapter default. Falls back to "medium".
|
|
16961
|
+
*/
|
|
16429
16962
|
getVerbosity() {
|
|
16430
16963
|
const config = this.context.configManager.get();
|
|
16431
16964
|
const channelConfig = config.channels;
|
|
@@ -16434,6 +16967,13 @@ var init_messaging_adapter = __esm({
|
|
|
16434
16967
|
if (v === "low" || v === "high") return v;
|
|
16435
16968
|
return "medium";
|
|
16436
16969
|
}
|
|
16970
|
+
/**
|
|
16971
|
+
* Determines whether a message should be displayed at the given verbosity.
|
|
16972
|
+
*
|
|
16973
|
+
* Noise filtering: tool calls matching noise rules (e.g., `ls`, `glob`, `grep`)
|
|
16974
|
+
* are hidden at medium/low verbosity to reduce clutter. Thoughts and usage
|
|
16975
|
+
* stats are hidden entirely at "low".
|
|
16976
|
+
*/
|
|
16437
16977
|
shouldDisplay(content, verbosity) {
|
|
16438
16978
|
if (verbosity === "low" && HIDDEN_ON_LOW.has(content.type)) return false;
|
|
16439
16979
|
if (content.type === "tool_call") {
|
|
@@ -16665,6 +17205,7 @@ var init_output_mode_resolver = __esm({
|
|
|
16665
17205
|
"use strict";
|
|
16666
17206
|
VALID_MODES = /* @__PURE__ */ new Set(["low", "medium", "high"]);
|
|
16667
17207
|
OutputModeResolver = class {
|
|
17208
|
+
/** Resolves the effective output mode by walking the override cascade. */
|
|
16668
17209
|
resolve(configManager, adapterName, sessionId, sessionManager) {
|
|
16669
17210
|
const config = configManager.get();
|
|
16670
17211
|
let mode = toOutputMode(config.outputMode) ?? "medium";
|
|
@@ -16763,7 +17304,11 @@ var init_adapter2 = __esm({
|
|
|
16763
17304
|
_topicsInitialized = false;
|
|
16764
17305
|
/** Background watcher timer — cancelled on stop() or when topics succeed */
|
|
16765
17306
|
_prerequisiteWatcher = null;
|
|
16766
|
-
/**
|
|
17307
|
+
/**
|
|
17308
|
+
* Persist the control message ID both in-memory and to the session record.
|
|
17309
|
+
* The control message is the pinned status card with bypass/TTS buttons; its ID
|
|
17310
|
+
* is needed after a restart to edit it when config changes.
|
|
17311
|
+
*/
|
|
16767
17312
|
storeControlMsgId(sessionId, msgId) {
|
|
16768
17313
|
this.controlMsgIds.set(sessionId, msgId);
|
|
16769
17314
|
const record = this.core.sessionManager.getSessionRecord(sessionId);
|
|
@@ -16828,6 +17373,12 @@ var init_adapter2 = __esm({
|
|
|
16828
17373
|
this.telegramConfig = config;
|
|
16829
17374
|
this.saveTopicIds = saveTopicIds;
|
|
16830
17375
|
}
|
|
17376
|
+
/**
|
|
17377
|
+
* Set up the grammY bot, register all callback and message handlers, then perform
|
|
17378
|
+
* two-phase startup: Phase 1 starts polling immediately; Phase 2 checks group
|
|
17379
|
+
* prerequisites (bot is admin, topics are enabled) and creates/restores system topics.
|
|
17380
|
+
* If prerequisites are not met, a background watcher retries until they are.
|
|
17381
|
+
*/
|
|
16831
17382
|
async start() {
|
|
16832
17383
|
this.bot = new Bot(this.telegramConfig.botToken, {
|
|
16833
17384
|
client: {
|
|
@@ -17258,6 +17809,13 @@ OpenACP will automatically retry until this is resolved.`;
|
|
|
17258
17809
|
};
|
|
17259
17810
|
this._prerequisiteWatcher = setTimeout(retry, schedule[0]);
|
|
17260
17811
|
}
|
|
17812
|
+
/**
|
|
17813
|
+
* Tear down the bot and release all associated resources.
|
|
17814
|
+
*
|
|
17815
|
+
* Cancels the background prerequisite watcher, destroys all per-session activity
|
|
17816
|
+
* trackers (which hold interval timers), removes eventBus listeners, clears the
|
|
17817
|
+
* send queue, and stops the grammY bot polling loop.
|
|
17818
|
+
*/
|
|
17261
17819
|
async stop() {
|
|
17262
17820
|
if (this._prerequisiteWatcher !== null) {
|
|
17263
17821
|
clearTimeout(this._prerequisiteWatcher);
|
|
@@ -17391,8 +17949,7 @@ ${lines.join("\n")}`;
|
|
|
17391
17949
|
await this.draftManager.finalize(sessionId, assistantSession?.id);
|
|
17392
17950
|
}
|
|
17393
17951
|
if (sessionId) {
|
|
17394
|
-
|
|
17395
|
-
if (tracker) await tracker.onNewPrompt();
|
|
17952
|
+
await this.drainAndResetTracker(sessionId);
|
|
17396
17953
|
}
|
|
17397
17954
|
ctx.replyWithChatAction("typing").catch(() => {
|
|
17398
17955
|
});
|
|
@@ -17481,9 +18038,26 @@ ${lines.join("\n")}`;
|
|
|
17481
18038
|
* its creation event. This queue ensures events are processed in the order they arrive.
|
|
17482
18039
|
*/
|
|
17483
18040
|
_dispatchQueues = /* @__PURE__ */ new Map();
|
|
18041
|
+
/**
|
|
18042
|
+
* Drain pending event dispatches from the previous prompt, then reset the
|
|
18043
|
+
* activity tracker so late tool_call events don't leak into the new card.
|
|
18044
|
+
*/
|
|
18045
|
+
async drainAndResetTracker(sessionId) {
|
|
18046
|
+
const pendingDispatch = this._dispatchQueues.get(sessionId);
|
|
18047
|
+
if (pendingDispatch) await pendingDispatch;
|
|
18048
|
+
const tracker = this.sessionTrackers.get(sessionId);
|
|
18049
|
+
if (tracker) await tracker.onNewPrompt();
|
|
18050
|
+
}
|
|
17484
18051
|
getTracer(sessionId) {
|
|
17485
18052
|
return this.core.sessionManager.getSession(sessionId)?.agentInstance?.debugTracer ?? null;
|
|
17486
18053
|
}
|
|
18054
|
+
/**
|
|
18055
|
+
* Primary outbound dispatch method — routes an agent message to the session's Telegram topic.
|
|
18056
|
+
*
|
|
18057
|
+
* Wraps the base class `sendMessage` in a per-session promise chain (`_dispatchQueues`)
|
|
18058
|
+
* so that concurrent events fired from SessionBridge are serialized and delivered in the
|
|
18059
|
+
* order they arrive, preventing fast handlers from overtaking slower ones.
|
|
18060
|
+
*/
|
|
17487
18061
|
async sendMessage(sessionId, content) {
|
|
17488
18062
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
17489
18063
|
if (!session) return;
|
|
@@ -17797,6 +18371,11 @@ Task completed.
|
|
|
17797
18371
|
)
|
|
17798
18372
|
);
|
|
17799
18373
|
}
|
|
18374
|
+
/**
|
|
18375
|
+
* Render a PermissionRequest as an inline keyboard in the session topic and
|
|
18376
|
+
* notify the Notifications topic. Runs inside a sendQueue item, so
|
|
18377
|
+
* notification is fire-and-forget to avoid deadlock.
|
|
18378
|
+
*/
|
|
17800
18379
|
async sendPermissionRequest(sessionId, request) {
|
|
17801
18380
|
this.getTracer(sessionId)?.log("telegram", { action: "permission:send", sessionId, requestId: request.id, description: request.description });
|
|
17802
18381
|
log29.info({ sessionId, requestId: request.id }, "Permission request sent");
|
|
@@ -17806,6 +18385,11 @@ Task completed.
|
|
|
17806
18385
|
() => this.permissionHandler.sendPermissionRequest(session, request)
|
|
17807
18386
|
);
|
|
17808
18387
|
}
|
|
18388
|
+
/**
|
|
18389
|
+
* Post a notification to the Notifications topic.
|
|
18390
|
+
* Assistant session notifications are suppressed — the assistant topic is
|
|
18391
|
+
* the user's primary interface and does not need a separate alert.
|
|
18392
|
+
*/
|
|
17809
18393
|
async sendNotification(notification) {
|
|
17810
18394
|
this.getTracer(notification.sessionId)?.log("telegram", { action: "notification:send", sessionId: notification.sessionId, type: notification.type });
|
|
17811
18395
|
if (notification.sessionId === this.core.assistantManager?.get("telegram")?.id) return;
|
|
@@ -17846,6 +18430,10 @@ Task completed.
|
|
|
17846
18430
|
})
|
|
17847
18431
|
);
|
|
17848
18432
|
}
|
|
18433
|
+
/**
|
|
18434
|
+
* Create a new Telegram forum topic for a session and return its thread ID as a string.
|
|
18435
|
+
* Called by the core when a session is created via the API or CLI (not from the Telegram UI).
|
|
18436
|
+
*/
|
|
17849
18437
|
async createSessionThread(sessionId, name) {
|
|
17850
18438
|
this.getTracer(sessionId)?.log("telegram", { action: "thread:create", sessionId, name });
|
|
17851
18439
|
log29.info({ sessionId, name }, "Session topic created");
|
|
@@ -17853,6 +18441,10 @@ Task completed.
|
|
|
17853
18441
|
await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
|
|
17854
18442
|
);
|
|
17855
18443
|
}
|
|
18444
|
+
/**
|
|
18445
|
+
* Rename the forum topic for a session and update the session record's display name.
|
|
18446
|
+
* No-ops silently if the session doesn't have a threadId yet (e.g. still initializing).
|
|
18447
|
+
*/
|
|
17856
18448
|
async renameSessionThread(sessionId, newName) {
|
|
17857
18449
|
this.getTracer(sessionId)?.log("telegram", { action: "thread:rename", sessionId, newName });
|
|
17858
18450
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
@@ -17870,6 +18462,7 @@ Task completed.
|
|
|
17870
18462
|
);
|
|
17871
18463
|
await this.core.sessionManager.patchRecord(sessionId, { name: newName });
|
|
17872
18464
|
}
|
|
18465
|
+
/** Delete the forum topic associated with a session. */
|
|
17873
18466
|
async deleteSessionThread(sessionId) {
|
|
17874
18467
|
const record = this.core.sessionManager.getSessionRecord(sessionId);
|
|
17875
18468
|
const platform2 = record?.platform;
|
|
@@ -17884,6 +18477,11 @@ Task completed.
|
|
|
17884
18477
|
);
|
|
17885
18478
|
}
|
|
17886
18479
|
}
|
|
18480
|
+
/**
|
|
18481
|
+
* Display or update the pinned skill commands message for a session.
|
|
18482
|
+
* If the session's threadId is not yet set (e.g. session created from API),
|
|
18483
|
+
* the commands are queued and flushed once the thread becomes available.
|
|
18484
|
+
*/
|
|
17887
18485
|
async sendSkillCommands(sessionId, commands) {
|
|
17888
18486
|
if (sessionId === this.core.assistantManager?.get("telegram")?.id) return;
|
|
17889
18487
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
@@ -17968,7 +18566,10 @@ Task completed.
|
|
|
17968
18566
|
return;
|
|
17969
18567
|
}
|
|
17970
18568
|
const sid = await this.resolveSessionId(threadId);
|
|
17971
|
-
if (sid)
|
|
18569
|
+
if (sid) {
|
|
18570
|
+
await this.draftManager.finalize(sid, this.core.assistantManager?.get("telegram")?.id);
|
|
18571
|
+
await this.drainAndResetTracker(sid);
|
|
18572
|
+
}
|
|
17972
18573
|
this.core.handleMessage({
|
|
17973
18574
|
channelId: "telegram",
|
|
17974
18575
|
threadId: String(threadId),
|
|
@@ -17977,10 +18578,24 @@ Task completed.
|
|
|
17977
18578
|
attachments: [att]
|
|
17978
18579
|
}).catch((err) => log29.error({ err }, "handleMessage error"));
|
|
17979
18580
|
}
|
|
18581
|
+
/**
|
|
18582
|
+
* Remove skill slash commands from the Telegram bot command list for a session.
|
|
18583
|
+
*
|
|
18584
|
+
* Clears any queued pending commands that hadn't been sent yet, then delegates
|
|
18585
|
+
* to `SkillCommandManager` to delete the commands from the Telegram API. Called
|
|
18586
|
+
* when a session with registered skill commands ends.
|
|
18587
|
+
*/
|
|
17980
18588
|
async cleanupSkillCommands(sessionId) {
|
|
17981
18589
|
this._pendingSkillCommands.delete(sessionId);
|
|
17982
18590
|
await this.skillManager.cleanup(sessionId);
|
|
17983
18591
|
}
|
|
18592
|
+
/**
|
|
18593
|
+
* Clean up all adapter state associated with a session.
|
|
18594
|
+
*
|
|
18595
|
+
* Finalizes and discards any in-flight draft, destroys the activity tracker
|
|
18596
|
+
* (stopping ThinkingIndicator timers and finalizing any open ToolCard), and
|
|
18597
|
+
* clears pending skill commands. Called when a session ends or is reset.
|
|
18598
|
+
*/
|
|
17984
18599
|
async cleanupSessionState(sessionId) {
|
|
17985
18600
|
this._pendingSkillCommands.delete(sessionId);
|
|
17986
18601
|
await this.draftManager.finalize(sessionId, this.core.assistantManager?.get("telegram")?.id);
|
|
@@ -17991,9 +18606,22 @@ Task completed.
|
|
|
17991
18606
|
this.sessionTrackers.delete(sessionId);
|
|
17992
18607
|
}
|
|
17993
18608
|
}
|
|
18609
|
+
/**
|
|
18610
|
+
* Remove `[TTS]...[/TTS]` blocks from the active or finalized draft for a session.
|
|
18611
|
+
*
|
|
18612
|
+
* The agent embeds these blocks so the speech plugin can extract the TTS text, but
|
|
18613
|
+
* they should never appear in the chat message. Called after TTS audio has been sent.
|
|
18614
|
+
*/
|
|
17994
18615
|
async stripTTSBlock(sessionId) {
|
|
17995
18616
|
await this.draftManager.stripPattern(sessionId, /\[TTS\][\s\S]*?\[\/TTS\]/g);
|
|
17996
18617
|
}
|
|
18618
|
+
/**
|
|
18619
|
+
* Archive a session by deleting its forum topic.
|
|
18620
|
+
*
|
|
18621
|
+
* Sets `session.archiving = true` to suppress any outgoing messages while the
|
|
18622
|
+
* topic is being torn down, finalizes pending drafts, cleans up all trackers,
|
|
18623
|
+
* then deletes the Telegram topic (which removes all messages).
|
|
18624
|
+
*/
|
|
17997
18625
|
async archiveSessionTopic(sessionId) {
|
|
17998
18626
|
this.getTracer(sessionId)?.log("telegram", { action: "thread:archive", sessionId });
|
|
17999
18627
|
const core = this.core;
|
|
@@ -19022,13 +19650,18 @@ var init_path_guard = __esm({
|
|
|
19022
19650
|
"src/core/security/path-guard.ts"() {
|
|
19023
19651
|
"use strict";
|
|
19024
19652
|
DEFAULT_DENY_PATTERNS = [
|
|
19653
|
+
// Environment files — contain API keys, database URLs, etc.
|
|
19025
19654
|
".env",
|
|
19026
19655
|
".env.*",
|
|
19656
|
+
// Cryptographic keys
|
|
19027
19657
|
"*.key",
|
|
19028
19658
|
"*.pem",
|
|
19659
|
+
// SSH and cloud credentials
|
|
19029
19660
|
".ssh/",
|
|
19030
19661
|
".aws/",
|
|
19662
|
+
// OpenACP workspace — contains bot tokens and secrets
|
|
19031
19663
|
".openacp/",
|
|
19664
|
+
// Generic credential/secret files
|
|
19032
19665
|
"**/credentials*",
|
|
19033
19666
|
"**/secrets*",
|
|
19034
19667
|
"**/*.secret"
|
|
@@ -19056,6 +19689,20 @@ var init_path_guard = __esm({
|
|
|
19056
19689
|
this.ig.add(options.ignorePatterns);
|
|
19057
19690
|
}
|
|
19058
19691
|
}
|
|
19692
|
+
/**
|
|
19693
|
+
* Checks whether an agent is allowed to access the given path.
|
|
19694
|
+
*
|
|
19695
|
+
* Validation order:
|
|
19696
|
+
* 1. Write to .openacpignore is always blocked (prevents agents from weakening their own restrictions)
|
|
19697
|
+
* 2. Path must be within cwd or an explicitly allowlisted path
|
|
19698
|
+
* 3. If within cwd but not allowlisted, path must not match any deny pattern
|
|
19699
|
+
*
|
|
19700
|
+
* @param targetPath - The path the agent is attempting to access.
|
|
19701
|
+
* @param operation - The operation type. Write operations are subject to stricter
|
|
19702
|
+
* restrictions than reads — specifically, writing to `.openacpignore` is blocked
|
|
19703
|
+
* (to prevent agents from weakening their own restrictions), while reading it is allowed.
|
|
19704
|
+
* @returns `{ allowed: true }` or `{ allowed: false, reason: "..." }`
|
|
19705
|
+
*/
|
|
19059
19706
|
validatePath(targetPath, operation) {
|
|
19060
19707
|
const resolved = path43.resolve(targetPath);
|
|
19061
19708
|
let realPath;
|
|
@@ -19091,6 +19738,7 @@ var init_path_guard = __esm({
|
|
|
19091
19738
|
}
|
|
19092
19739
|
return { allowed: true, reason: "" };
|
|
19093
19740
|
}
|
|
19741
|
+
/** Adds an additional allowed path at runtime (e.g. for file-service uploads). */
|
|
19094
19742
|
addAllowedPath(p2) {
|
|
19095
19743
|
try {
|
|
19096
19744
|
this.allowedPaths.push(fs38.realpathSync(path43.resolve(p2)));
|
|
@@ -19098,6 +19746,10 @@ var init_path_guard = __esm({
|
|
|
19098
19746
|
this.allowedPaths.push(path43.resolve(p2));
|
|
19099
19747
|
}
|
|
19100
19748
|
}
|
|
19749
|
+
/**
|
|
19750
|
+
* Loads additional deny patterns from .openacpignore in the workspace root.
|
|
19751
|
+
* Follows .gitignore syntax — blank lines and lines starting with # are skipped.
|
|
19752
|
+
*/
|
|
19101
19753
|
static loadIgnoreFile(cwd) {
|
|
19102
19754
|
const ignorePath = path43.join(cwd, ".openacpignore");
|
|
19103
19755
|
try {
|
|
@@ -19137,6 +19789,7 @@ var init_env_filter = __esm({
|
|
|
19137
19789
|
"src/core/security/env-filter.ts"() {
|
|
19138
19790
|
"use strict";
|
|
19139
19791
|
DEFAULT_ENV_WHITELIST = [
|
|
19792
|
+
// Shell basics — agents need these to resolve commands and write temp files
|
|
19140
19793
|
"PATH",
|
|
19141
19794
|
"HOME",
|
|
19142
19795
|
"SHELL",
|
|
@@ -19149,11 +19802,11 @@ var init_env_filter = __esm({
|
|
|
19149
19802
|
"XDG_*",
|
|
19150
19803
|
"NODE_ENV",
|
|
19151
19804
|
"EDITOR",
|
|
19152
|
-
// Git
|
|
19805
|
+
// Git — agents need git config and SSH access for code operations
|
|
19153
19806
|
"GIT_*",
|
|
19154
19807
|
"SSH_AUTH_SOCK",
|
|
19155
19808
|
"SSH_AGENT_PID",
|
|
19156
|
-
// Terminal rendering
|
|
19809
|
+
// Terminal rendering — ensures correct color output in agent responses
|
|
19157
19810
|
"COLORTERM",
|
|
19158
19811
|
"FORCE_COLOR",
|
|
19159
19812
|
"NO_COLOR",
|
|
@@ -19221,12 +19874,14 @@ var init_stderr_capture = __esm({
|
|
|
19221
19874
|
this.maxLines = maxLines;
|
|
19222
19875
|
}
|
|
19223
19876
|
lines = [];
|
|
19877
|
+
/** Append a chunk of stderr output, splitting on newlines and trimming to maxLines. */
|
|
19224
19878
|
append(chunk) {
|
|
19225
19879
|
this.lines.push(...chunk.split("\n").filter(Boolean));
|
|
19226
19880
|
if (this.lines.length > this.maxLines) {
|
|
19227
19881
|
this.lines = this.lines.slice(-this.maxLines);
|
|
19228
19882
|
}
|
|
19229
19883
|
}
|
|
19884
|
+
/** Return all captured lines joined as a single string. */
|
|
19230
19885
|
getLastLines() {
|
|
19231
19886
|
return this.lines.join("\n");
|
|
19232
19887
|
}
|
|
@@ -19245,6 +19900,7 @@ var init_typed_emitter = __esm({
|
|
|
19245
19900
|
listeners = /* @__PURE__ */ new Map();
|
|
19246
19901
|
paused = false;
|
|
19247
19902
|
buffer = [];
|
|
19903
|
+
/** Register a listener for the given event. Returns `this` for chaining. */
|
|
19248
19904
|
on(event, listener) {
|
|
19249
19905
|
let set = this.listeners.get(event);
|
|
19250
19906
|
if (!set) {
|
|
@@ -19254,10 +19910,17 @@ var init_typed_emitter = __esm({
|
|
|
19254
19910
|
set.add(listener);
|
|
19255
19911
|
return this;
|
|
19256
19912
|
}
|
|
19913
|
+
/** Remove a specific listener for the given event. */
|
|
19257
19914
|
off(event, listener) {
|
|
19258
19915
|
this.listeners.get(event)?.delete(listener);
|
|
19259
19916
|
return this;
|
|
19260
19917
|
}
|
|
19918
|
+
/**
|
|
19919
|
+
* Emit an event to all registered listeners.
|
|
19920
|
+
*
|
|
19921
|
+
* When paused, events are buffered (up to MAX_BUFFER_SIZE) unless
|
|
19922
|
+
* the passthrough filter allows them through immediately.
|
|
19923
|
+
*/
|
|
19261
19924
|
emit(event, ...args2) {
|
|
19262
19925
|
if (this.paused) {
|
|
19263
19926
|
if (this.passthroughFn?.(event, args2)) {
|
|
@@ -19301,6 +19964,7 @@ var init_typed_emitter = __esm({
|
|
|
19301
19964
|
get bufferSize() {
|
|
19302
19965
|
return this.buffer.length;
|
|
19303
19966
|
}
|
|
19967
|
+
/** Remove all listeners for a specific event, or all events if none specified. */
|
|
19304
19968
|
removeAllListeners(event) {
|
|
19305
19969
|
if (event) {
|
|
19306
19970
|
this.listeners.delete(event);
|
|
@@ -19308,6 +19972,7 @@ var init_typed_emitter = __esm({
|
|
|
19308
19972
|
this.listeners.clear();
|
|
19309
19973
|
}
|
|
19310
19974
|
}
|
|
19975
|
+
/** Deliver an event to listeners, isolating errors so one broken listener doesn't break others. */
|
|
19311
19976
|
deliver(event, args2) {
|
|
19312
19977
|
const set = this.listeners.get(event);
|
|
19313
19978
|
if (!set) return;
|
|
@@ -19379,6 +20044,11 @@ var init_terminal_manager = __esm({
|
|
|
19379
20044
|
constructor(maxOutputBytes = 1024 * 1024) {
|
|
19380
20045
|
this.maxOutputBytes = maxOutputBytes;
|
|
19381
20046
|
}
|
|
20047
|
+
/**
|
|
20048
|
+
* Spawn a new terminal process. Runs terminal:beforeCreate middleware first
|
|
20049
|
+
* (which can modify command/args/env or block creation entirely).
|
|
20050
|
+
* Returns a terminalId for subsequent output/wait/kill operations.
|
|
20051
|
+
*/
|
|
19382
20052
|
async createTerminal(sessionId, params, middlewareChain) {
|
|
19383
20053
|
let termCommand = params.command;
|
|
19384
20054
|
let termArgs = params.args ?? [];
|
|
@@ -19460,6 +20130,7 @@ var init_terminal_manager = __esm({
|
|
|
19460
20130
|
});
|
|
19461
20131
|
return { terminalId };
|
|
19462
20132
|
}
|
|
20133
|
+
/** Retrieve accumulated stdout/stderr output for a terminal. */
|
|
19463
20134
|
getOutput(terminalId) {
|
|
19464
20135
|
const state = this.terminals.get(terminalId);
|
|
19465
20136
|
if (!state) {
|
|
@@ -19474,6 +20145,7 @@ var init_terminal_manager = __esm({
|
|
|
19474
20145
|
} : void 0
|
|
19475
20146
|
};
|
|
19476
20147
|
}
|
|
20148
|
+
/** Block until the terminal process exits, returning exit code and signal. */
|
|
19477
20149
|
async waitForExit(terminalId) {
|
|
19478
20150
|
const state = this.terminals.get(terminalId);
|
|
19479
20151
|
if (!state) {
|
|
@@ -19497,6 +20169,7 @@ var init_terminal_manager = __esm({
|
|
|
19497
20169
|
}
|
|
19498
20170
|
});
|
|
19499
20171
|
}
|
|
20172
|
+
/** Send SIGTERM to a terminal process (graceful shutdown). */
|
|
19500
20173
|
kill(terminalId) {
|
|
19501
20174
|
const state = this.terminals.get(terminalId);
|
|
19502
20175
|
if (!state) {
|
|
@@ -19504,6 +20177,7 @@ var init_terminal_manager = __esm({
|
|
|
19504
20177
|
}
|
|
19505
20178
|
state.process.kill("SIGTERM");
|
|
19506
20179
|
}
|
|
20180
|
+
/** Force-kill (SIGKILL) and immediately remove a terminal from the registry. */
|
|
19507
20181
|
release(terminalId) {
|
|
19508
20182
|
const state = this.terminals.get(terminalId);
|
|
19509
20183
|
if (!state) {
|
|
@@ -19512,6 +20186,7 @@ var init_terminal_manager = __esm({
|
|
|
19512
20186
|
state.process.kill("SIGKILL");
|
|
19513
20187
|
this.terminals.delete(terminalId);
|
|
19514
20188
|
}
|
|
20189
|
+
/** Force-kill all terminals. Used during session/system teardown. */
|
|
19515
20190
|
destroyAll() {
|
|
19516
20191
|
for (const [, t] of this.terminals) {
|
|
19517
20192
|
t.process.kill("SIGKILL");
|
|
@@ -19559,6 +20234,12 @@ var init_debug_tracer = __esm({
|
|
|
19559
20234
|
}
|
|
19560
20235
|
dirCreated = false;
|
|
19561
20236
|
logDir;
|
|
20237
|
+
/**
|
|
20238
|
+
* Write a timestamped JSONL entry to the trace file for the given layer.
|
|
20239
|
+
*
|
|
20240
|
+
* Handles circular references gracefully and silently swallows errors —
|
|
20241
|
+
* debug logging must never crash the application.
|
|
20242
|
+
*/
|
|
19562
20243
|
log(layer, data) {
|
|
19563
20244
|
try {
|
|
19564
20245
|
if (!this.dirCreated) {
|
|
@@ -19716,9 +20397,13 @@ var init_agent_instance = __esm({
|
|
|
19716
20397
|
connection;
|
|
19717
20398
|
child;
|
|
19718
20399
|
stderrCapture;
|
|
20400
|
+
/** Manages terminal subprocesses that agents can spawn for shell commands. */
|
|
19719
20401
|
terminalManager = new TerminalManager();
|
|
20402
|
+
/** Shared across all instances — resolves MCP server configs for ACP sessions. */
|
|
19720
20403
|
static mcpManager = new McpManager();
|
|
20404
|
+
/** Guards against emitting crash events during intentional shutdown. */
|
|
19721
20405
|
_destroying = false;
|
|
20406
|
+
/** Restricts agent file I/O to the workspace directory and explicitly allowed paths. */
|
|
19722
20407
|
pathGuard;
|
|
19723
20408
|
sessionId;
|
|
19724
20409
|
agentName;
|
|
@@ -19728,16 +20413,36 @@ var init_agent_instance = __esm({
|
|
|
19728
20413
|
initialSessionResponse;
|
|
19729
20414
|
middlewareChain;
|
|
19730
20415
|
debugTracer = null;
|
|
19731
|
-
/**
|
|
20416
|
+
/**
|
|
20417
|
+
* Whitelist an additional filesystem path for agent read access.
|
|
20418
|
+
*
|
|
20419
|
+
* Called by SessionFactory to allow agents to read files outside the
|
|
20420
|
+
* workspace (e.g., the file-service upload directory for attachments).
|
|
20421
|
+
*/
|
|
19732
20422
|
addAllowedPath(p2) {
|
|
19733
20423
|
this.pathGuard.addAllowedPath(p2);
|
|
19734
20424
|
}
|
|
19735
|
-
// Callback — set by
|
|
20425
|
+
// Callback — set by Session/Core when wiring events. Returns the selected
|
|
20426
|
+
// permission option ID. Default no-op auto-selects the first option.
|
|
19736
20427
|
onPermissionRequest = async () => "";
|
|
19737
20428
|
constructor(agentName) {
|
|
19738
20429
|
super();
|
|
19739
20430
|
this.agentName = agentName;
|
|
19740
20431
|
}
|
|
20432
|
+
/**
|
|
20433
|
+
* Spawn the agent child process and complete the ACP protocol handshake.
|
|
20434
|
+
*
|
|
20435
|
+
* Steps:
|
|
20436
|
+
* 1. Resolve the agent command to a directly executable path
|
|
20437
|
+
* 2. Create a PathGuard scoped to the working directory
|
|
20438
|
+
* 3. Spawn the subprocess with a filtered environment (security: only whitelisted
|
|
20439
|
+
* env vars are passed to prevent leaking secrets like API keys)
|
|
20440
|
+
* 4. Wire stdin/stdout through debug-tracing Transform streams
|
|
20441
|
+
* 5. Convert Node streams → Web streams for the ACP SDK
|
|
20442
|
+
* 6. Perform the ACP `initialize` handshake and negotiate capabilities
|
|
20443
|
+
*
|
|
20444
|
+
* Does NOT create a session — callers must follow up with newSession or loadSession.
|
|
20445
|
+
*/
|
|
19741
20446
|
static async spawnSubprocess(agentDef, workingDirectory, allowedPaths = []) {
|
|
19742
20447
|
const instance = new _AgentInstance(agentDef.name);
|
|
19743
20448
|
const resolved = resolveAgentCommand(agentDef.command);
|
|
@@ -19836,6 +20541,13 @@ var init_agent_instance = __esm({
|
|
|
19836
20541
|
);
|
|
19837
20542
|
return instance;
|
|
19838
20543
|
}
|
|
20544
|
+
/**
|
|
20545
|
+
* Monitor the subprocess for unexpected exits and emit error events.
|
|
20546
|
+
*
|
|
20547
|
+
* Distinguishes intentional shutdown (SIGTERM/SIGINT during destroy) from
|
|
20548
|
+
* crashes (non-zero exit code or unexpected signal). Crash events include
|
|
20549
|
+
* captured stderr output for diagnostic context.
|
|
20550
|
+
*/
|
|
19839
20551
|
setupCrashDetection() {
|
|
19840
20552
|
this.child.on("exit", (code, signal) => {
|
|
19841
20553
|
if (this._destroying) return;
|
|
@@ -19858,6 +20570,18 @@ ${stderr}`
|
|
|
19858
20570
|
log33.debug({ sessionId: this.sessionId }, "ACP connection closed");
|
|
19859
20571
|
});
|
|
19860
20572
|
}
|
|
20573
|
+
/**
|
|
20574
|
+
* Spawn a new agent subprocess and create a fresh ACP session.
|
|
20575
|
+
*
|
|
20576
|
+
* This is the primary entry point for starting an agent. It spawns the
|
|
20577
|
+
* subprocess, completes the ACP handshake, and calls `newSession` to
|
|
20578
|
+
* initialize the agent's working context (cwd, MCP servers).
|
|
20579
|
+
*
|
|
20580
|
+
* @param agentDef - Agent definition (command, args, env) from the catalog
|
|
20581
|
+
* @param workingDirectory - Workspace root the agent operates in
|
|
20582
|
+
* @param mcpServers - Optional MCP server configs to extend agent capabilities
|
|
20583
|
+
* @param allowedPaths - Extra filesystem paths the agent may access
|
|
20584
|
+
*/
|
|
19861
20585
|
static async spawn(agentDef, workingDirectory, mcpServers, allowedPaths) {
|
|
19862
20586
|
log33.debug(
|
|
19863
20587
|
{ agentName: agentDef.name, command: agentDef.command },
|
|
@@ -19891,6 +20615,15 @@ ${stderr}`
|
|
|
19891
20615
|
);
|
|
19892
20616
|
return instance;
|
|
19893
20617
|
}
|
|
20618
|
+
/**
|
|
20619
|
+
* Spawn a new subprocess and restore an existing agent session.
|
|
20620
|
+
*
|
|
20621
|
+
* Tries loadSession first (preferred, stable API), falls back to the
|
|
20622
|
+
* unstable resumeSession, and finally falls back to creating a brand-new
|
|
20623
|
+
* session if resume fails entirely (e.g., agent lost its state).
|
|
20624
|
+
*
|
|
20625
|
+
* @param agentSessionId - The agent-side session ID to restore
|
|
20626
|
+
*/
|
|
19894
20627
|
static async resume(agentDef, workingDirectory, agentSessionId, mcpServers, allowedPaths) {
|
|
19895
20628
|
log33.debug({ agentName: agentDef.name, agentSessionId }, "Resuming agent");
|
|
19896
20629
|
const spawnStart = Date.now();
|
|
@@ -19957,12 +20690,26 @@ ${stderr}`
|
|
|
19957
20690
|
instance.setupCrashDetection();
|
|
19958
20691
|
return instance;
|
|
19959
20692
|
}
|
|
19960
|
-
|
|
20693
|
+
/**
|
|
20694
|
+
* Build the ACP Client callback object.
|
|
20695
|
+
*
|
|
20696
|
+
* The ACP SDK invokes these callbacks when the agent sends notifications
|
|
20697
|
+
* or requests. Each callback maps an ACP protocol message to either:
|
|
20698
|
+
* - An internal AgentEvent (emitted for Session/adapters to consume)
|
|
20699
|
+
* - A filesystem or terminal operation (executed on the agent's behalf)
|
|
20700
|
+
* - A permission request (proxied to the user via the adapter)
|
|
20701
|
+
*/
|
|
19961
20702
|
createClient(_agent) {
|
|
19962
20703
|
const self = this;
|
|
19963
20704
|
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
19964
20705
|
return {
|
|
19965
20706
|
// ── Session updates ──────────────────────────────────────────────────
|
|
20707
|
+
// The agent streams its response as a series of session update events.
|
|
20708
|
+
// Each event type maps to an internal AgentEvent that Session relays
|
|
20709
|
+
// to adapters for rendering (text chunks, tool calls, usage stats, etc.).
|
|
20710
|
+
// Chunks are forwarded to Session individually as they arrive — no buffering
|
|
20711
|
+
// happens at this layer. If buffering is needed (e.g., to avoid rate limits),
|
|
20712
|
+
// it is the responsibility of the Session or adapter layer.
|
|
19966
20713
|
async sessionUpdate(params) {
|
|
19967
20714
|
const update = params.update;
|
|
19968
20715
|
let event = null;
|
|
@@ -20091,6 +20838,10 @@ ${stderr}`
|
|
|
20091
20838
|
}
|
|
20092
20839
|
},
|
|
20093
20840
|
// ── Permission requests ──────────────────────────────────────────────
|
|
20841
|
+
// The agent needs user approval before performing sensitive operations
|
|
20842
|
+
// (e.g., file writes, shell commands). This proxies the request up
|
|
20843
|
+
// through Session → PermissionGate → adapter → user, then returns
|
|
20844
|
+
// the user's chosen option ID back to the agent.
|
|
20094
20845
|
async requestPermission(params) {
|
|
20095
20846
|
const permissionRequest = {
|
|
20096
20847
|
id: params.toolCall.toolCallId,
|
|
@@ -20107,6 +20858,9 @@ ${stderr}`
|
|
|
20107
20858
|
};
|
|
20108
20859
|
},
|
|
20109
20860
|
// ── File operations ──────────────────────────────────────────────────
|
|
20861
|
+
// The agent reads/writes files through these callbacks rather than
|
|
20862
|
+
// accessing the filesystem directly. This allows PathGuard to enforce
|
|
20863
|
+
// workspace boundaries and middleware hooks to intercept I/O.
|
|
20110
20864
|
async readTextFile(params) {
|
|
20111
20865
|
const p2 = params;
|
|
20112
20866
|
const pathCheck = self.pathGuard.validatePath(p2.path, "read");
|
|
@@ -20142,6 +20896,8 @@ ${stderr}`
|
|
|
20142
20896
|
return {};
|
|
20143
20897
|
},
|
|
20144
20898
|
// ── Terminal operations (delegated to TerminalManager) ─────────────
|
|
20899
|
+
// Agents can spawn shell commands via terminal operations. TerminalManager
|
|
20900
|
+
// handles subprocess lifecycle, output capture, and byte-limit enforcement.
|
|
20145
20901
|
async createTerminal(params) {
|
|
20146
20902
|
return self.terminalManager.createTerminal(
|
|
20147
20903
|
self.sessionId,
|
|
@@ -20171,6 +20927,13 @@ ${stderr}`
|
|
|
20171
20927
|
};
|
|
20172
20928
|
}
|
|
20173
20929
|
// ── New ACP methods ──────────────────────────────────────────────────
|
|
20930
|
+
/**
|
|
20931
|
+
* Update a session config option (mode, model, etc.) on the agent.
|
|
20932
|
+
*
|
|
20933
|
+
* Falls back to legacy `setSessionMode`/`unstable_setSessionModel` methods
|
|
20934
|
+
* for agents that haven't adopted the unified `session/set_config_option`
|
|
20935
|
+
* ACP method (detected via JSON-RPC -32601 "Method Not Found" error).
|
|
20936
|
+
*/
|
|
20174
20937
|
async setConfigOption(configId, value) {
|
|
20175
20938
|
try {
|
|
20176
20939
|
return await this.connection.setSessionConfigOption({
|
|
@@ -20198,12 +20961,14 @@ ${stderr}`
|
|
|
20198
20961
|
throw err;
|
|
20199
20962
|
}
|
|
20200
20963
|
}
|
|
20964
|
+
/** List the agent's known sessions, optionally filtered by working directory. */
|
|
20201
20965
|
async listSessions(cwd, cursor) {
|
|
20202
20966
|
return await this.connection.listSessions({
|
|
20203
20967
|
cwd: cwd ?? null,
|
|
20204
20968
|
cursor: cursor ?? null
|
|
20205
20969
|
});
|
|
20206
20970
|
}
|
|
20971
|
+
/** Load an existing agent session by ID into this subprocess. */
|
|
20207
20972
|
async loadSession(sessionId, cwd, mcpServers) {
|
|
20208
20973
|
const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
|
|
20209
20974
|
return await this.connection.loadSession({
|
|
@@ -20212,9 +20977,11 @@ ${stderr}`
|
|
|
20212
20977
|
mcpServers: resolvedMcp
|
|
20213
20978
|
});
|
|
20214
20979
|
}
|
|
20980
|
+
/** Trigger agent-managed authentication (e.g., OAuth flow). */
|
|
20215
20981
|
async authenticate(methodId) {
|
|
20216
20982
|
await this.connection.authenticate({ methodId });
|
|
20217
20983
|
}
|
|
20984
|
+
/** Fork an existing session, creating a new branch with shared history. */
|
|
20218
20985
|
async forkSession(sessionId, cwd, mcpServers) {
|
|
20219
20986
|
const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
|
|
20220
20987
|
return await this.connection.unstable_forkSession({
|
|
@@ -20223,10 +20990,25 @@ ${stderr}`
|
|
|
20223
20990
|
mcpServers: resolvedMcp
|
|
20224
20991
|
});
|
|
20225
20992
|
}
|
|
20993
|
+
/** Close a session on the agent side (cleanup agent-internal state). */
|
|
20226
20994
|
async closeSession(sessionId) {
|
|
20227
20995
|
await this.connection.unstable_closeSession({ sessionId });
|
|
20228
20996
|
}
|
|
20229
20997
|
// ── Prompt & lifecycle ──────────────────────────────────────────────
|
|
20998
|
+
/**
|
|
20999
|
+
* Send a user prompt to the agent and wait for the complete response.
|
|
21000
|
+
*
|
|
21001
|
+
* Builds ACP content blocks from the text and any attachments (images
|
|
21002
|
+
* are base64-encoded if the agent supports them, otherwise appended as
|
|
21003
|
+
* file paths). The promise resolves when the agent finishes responding;
|
|
21004
|
+
* streaming events arrive via the `agent_event` emitter during execution.
|
|
21005
|
+
*
|
|
21006
|
+
* Attachments that exceed size limits or use unsupported formats are
|
|
21007
|
+
* skipped with a note appended to the prompt text.
|
|
21008
|
+
*
|
|
21009
|
+
* Call `cancel()` to interrupt a running prompt; the agent will stop and
|
|
21010
|
+
* the promise resolves with partial results.
|
|
21011
|
+
*/
|
|
20230
21012
|
async prompt(text5, attachments) {
|
|
20231
21013
|
const contentBlocks = [{ type: "text", text: text5 }];
|
|
20232
21014
|
const capabilities = this.promptCapabilities ?? {};
|
|
@@ -20276,9 +21058,18 @@ ${skipNote}`;
|
|
|
20276
21058
|
prompt: contentBlocks
|
|
20277
21059
|
});
|
|
20278
21060
|
}
|
|
21061
|
+
/** Cancel the currently running prompt. The agent should stop and return partial results. */
|
|
20279
21062
|
async cancel() {
|
|
20280
21063
|
await this.connection.cancel({ sessionId: this.sessionId });
|
|
20281
21064
|
}
|
|
21065
|
+
/**
|
|
21066
|
+
* Gracefully shut down the agent subprocess.
|
|
21067
|
+
*
|
|
21068
|
+
* Sends SIGTERM first, giving the agent up to 10 seconds to clean up.
|
|
21069
|
+
* If the process hasn't exited by then, SIGKILL forces termination.
|
|
21070
|
+
* The timer is unref'd so it doesn't keep the Node process alive
|
|
21071
|
+
* during shutdown.
|
|
21072
|
+
*/
|
|
20282
21073
|
async destroy() {
|
|
20283
21074
|
this._destroying = true;
|
|
20284
21075
|
this.terminalManager.destroyAll();
|
|
@@ -20313,6 +21104,7 @@ var init_agent_manager = __esm({
|
|
|
20313
21104
|
constructor(catalog) {
|
|
20314
21105
|
this.catalog = catalog;
|
|
20315
21106
|
}
|
|
21107
|
+
/** Return definitions for all installed agents. */
|
|
20316
21108
|
getAvailableAgents() {
|
|
20317
21109
|
const installed = this.catalog.getInstalledEntries();
|
|
20318
21110
|
return Object.entries(installed).map(([key, agent]) => ({
|
|
@@ -20322,14 +21114,25 @@ var init_agent_manager = __esm({
|
|
|
20322
21114
|
env: agent.env
|
|
20323
21115
|
}));
|
|
20324
21116
|
}
|
|
21117
|
+
/** Look up a single agent definition by its short name (e.g., "claude", "gemini"). */
|
|
20325
21118
|
getAgent(name) {
|
|
20326
21119
|
return this.catalog.resolve(name);
|
|
20327
21120
|
}
|
|
21121
|
+
/**
|
|
21122
|
+
* Spawn a new agent subprocess with a fresh session.
|
|
21123
|
+
*
|
|
21124
|
+
* @throws If the agent is not installed — includes install instructions in the error message.
|
|
21125
|
+
*/
|
|
20328
21126
|
async spawn(agentName, workingDirectory, allowedPaths) {
|
|
20329
21127
|
const agentDef = this.getAgent(agentName);
|
|
20330
21128
|
if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
|
|
20331
21129
|
return AgentInstance.spawn(agentDef, workingDirectory, void 0, allowedPaths);
|
|
20332
21130
|
}
|
|
21131
|
+
/**
|
|
21132
|
+
* Spawn a subprocess and resume an existing agent session.
|
|
21133
|
+
*
|
|
21134
|
+
* Falls back to a new session if the agent cannot restore the given session ID.
|
|
21135
|
+
*/
|
|
20333
21136
|
async resume(agentName, workingDirectory, agentSessionId, allowedPaths) {
|
|
20334
21137
|
const agentDef = this.getAgent(agentName);
|
|
20335
21138
|
if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
|
|
@@ -20354,6 +21157,11 @@ var init_prompt_queue = __esm({
|
|
|
20354
21157
|
abortController = null;
|
|
20355
21158
|
/** Set when abort is triggered; drainNext waits for the current processor to settle before starting the next item. */
|
|
20356
21159
|
processorSettled = null;
|
|
21160
|
+
/**
|
|
21161
|
+
* Add a prompt to the queue. If no prompt is currently processing, it runs
|
|
21162
|
+
* immediately. Otherwise, it's buffered and the returned promise resolves
|
|
21163
|
+
* only after the prompt finishes processing.
|
|
21164
|
+
*/
|
|
20357
21165
|
async enqueue(text5, attachments, routing, turnId) {
|
|
20358
21166
|
if (this.processing) {
|
|
20359
21167
|
return new Promise((resolve9) => {
|
|
@@ -20362,6 +21170,7 @@ var init_prompt_queue = __esm({
|
|
|
20362
21170
|
}
|
|
20363
21171
|
await this.process(text5, attachments, routing, turnId);
|
|
20364
21172
|
}
|
|
21173
|
+
/** Run a single prompt through the processor, then drain the next queued item. */
|
|
20365
21174
|
async process(text5, attachments, routing, turnId) {
|
|
20366
21175
|
this.processing = true;
|
|
20367
21176
|
this.abortController = new AbortController();
|
|
@@ -20389,12 +21198,17 @@ var init_prompt_queue = __esm({
|
|
|
20389
21198
|
this.drainNext();
|
|
20390
21199
|
}
|
|
20391
21200
|
}
|
|
21201
|
+
/** Dequeue and process the next pending prompt, if any. Called after each prompt completes. */
|
|
20392
21202
|
drainNext() {
|
|
20393
21203
|
const next = this.queue.shift();
|
|
20394
21204
|
if (next) {
|
|
20395
21205
|
this.process(next.text, next.attachments, next.routing, next.turnId).then(next.resolve);
|
|
20396
21206
|
}
|
|
20397
21207
|
}
|
|
21208
|
+
/**
|
|
21209
|
+
* Abort the in-flight prompt and discard all queued prompts.
|
|
21210
|
+
* Pending promises are resolved (not rejected) so callers don't see unhandled rejections.
|
|
21211
|
+
*/
|
|
20398
21212
|
clear() {
|
|
20399
21213
|
if (this.abortController) {
|
|
20400
21214
|
this.abortController.abort();
|
|
@@ -20430,6 +21244,10 @@ var init_permission_gate = __esm({
|
|
|
20430
21244
|
constructor(timeoutMs) {
|
|
20431
21245
|
this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
20432
21246
|
}
|
|
21247
|
+
/**
|
|
21248
|
+
* Register a new permission request and return a promise that resolves with the
|
|
21249
|
+
* chosen option ID when the user responds, or rejects on timeout / supersession.
|
|
21250
|
+
*/
|
|
20433
21251
|
setPending(request) {
|
|
20434
21252
|
if (!this.settled && this.rejectFn) {
|
|
20435
21253
|
this.rejectFn(new Error("Superseded by new permission request"));
|
|
@@ -20448,6 +21266,7 @@ var init_permission_gate = __esm({
|
|
|
20448
21266
|
}
|
|
20449
21267
|
});
|
|
20450
21268
|
}
|
|
21269
|
+
/** Approve the pending request with the given option ID. No-op if already settled. */
|
|
20451
21270
|
resolve(optionId) {
|
|
20452
21271
|
if (this.settled || !this.resolveFn) return;
|
|
20453
21272
|
this.settled = true;
|
|
@@ -20455,6 +21274,7 @@ var init_permission_gate = __esm({
|
|
|
20455
21274
|
this.resolveFn(optionId);
|
|
20456
21275
|
this.cleanup();
|
|
20457
21276
|
}
|
|
21277
|
+
/** Deny the pending request. No-op if already settled. */
|
|
20458
21278
|
reject(reason) {
|
|
20459
21279
|
if (this.settled || !this.rejectFn) return;
|
|
20460
21280
|
this.settled = true;
|
|
@@ -20563,6 +21383,8 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
|
|
|
20563
21383
|
get agentInstance() {
|
|
20564
21384
|
return this._agentInstance;
|
|
20565
21385
|
}
|
|
21386
|
+
/** Setting agentInstance wires the agent→session event relay and commands buffer.
|
|
21387
|
+
* This happens both at construction and on agent switch (switchAgent). */
|
|
20566
21388
|
set agentInstance(agent) {
|
|
20567
21389
|
this._agentInstance = agent;
|
|
20568
21390
|
this.wireAgentRelay();
|
|
@@ -20665,7 +21487,7 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
|
|
|
20665
21487
|
this.transition("finished");
|
|
20666
21488
|
this.emit(SessionEv.SESSION_END, reason ?? "completed");
|
|
20667
21489
|
}
|
|
20668
|
-
/** Transition to cancelled — from active
|
|
21490
|
+
/** Transition to cancelled — from active or error (terminal session cancel) */
|
|
20669
21491
|
markCancelled() {
|
|
20670
21492
|
this.transition("cancelled");
|
|
20671
21493
|
}
|
|
@@ -20685,19 +21507,29 @@ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of y
|
|
|
20685
21507
|
get queueDepth() {
|
|
20686
21508
|
return this.queue.pending;
|
|
20687
21509
|
}
|
|
21510
|
+
/** Whether a prompt is currently being processed by the agent */
|
|
20688
21511
|
get promptRunning() {
|
|
20689
21512
|
return this.queue.isProcessing;
|
|
20690
21513
|
}
|
|
20691
21514
|
// --- Context Injection ---
|
|
21515
|
+
/** Store context markdown to be prepended to the next prompt (used for session resume with history). */
|
|
20692
21516
|
setContext(markdown) {
|
|
20693
21517
|
this.pendingContext = markdown;
|
|
20694
21518
|
}
|
|
20695
21519
|
// --- Voice Mode ---
|
|
21520
|
+
/** Set TTS mode: "off" = disabled, "next" = one-shot (auto-resets after prompt), "on" = persistent. */
|
|
20696
21521
|
setVoiceMode(mode) {
|
|
20697
21522
|
this.voiceMode = mode;
|
|
20698
21523
|
this.log.info({ voiceMode: mode }, "TTS mode changed");
|
|
20699
21524
|
}
|
|
20700
21525
|
// --- Public API ---
|
|
21526
|
+
/**
|
|
21527
|
+
* Enqueue a user prompt for serial processing.
|
|
21528
|
+
*
|
|
21529
|
+
* Runs the prompt through agent:beforePrompt middleware (which can modify or block),
|
|
21530
|
+
* then adds it to the PromptQueue. Returns a turnId that callers can use to correlate
|
|
21531
|
+
* queued/processing events before the prompt actually runs.
|
|
21532
|
+
*/
|
|
20701
21533
|
async enqueuePrompt(text5, attachments, routing, externalTurnId) {
|
|
20702
21534
|
const turnId = externalTurnId ?? nanoid5(8);
|
|
20703
21535
|
if (this.middlewareChain) {
|
|
@@ -20807,6 +21639,10 @@ ${text5}`;
|
|
|
20807
21639
|
await this.autoName();
|
|
20808
21640
|
}
|
|
20809
21641
|
}
|
|
21642
|
+
/**
|
|
21643
|
+
* Transcribe audio attachments to text if the agent doesn't support audio natively.
|
|
21644
|
+
* Audio attachments are removed and their transcriptions are appended to the prompt text.
|
|
21645
|
+
*/
|
|
20810
21646
|
async maybeTranscribeAudio(text5, attachments) {
|
|
20811
21647
|
if (!attachments?.length || !this.speechService) {
|
|
20812
21648
|
return { text: text5, attachments };
|
|
@@ -20852,6 +21688,7 @@ ${result.text}` : result.text;
|
|
|
20852
21688
|
attachments: remainingAttachments.length > 0 ? remainingAttachments : void 0
|
|
20853
21689
|
};
|
|
20854
21690
|
}
|
|
21691
|
+
/** Extract [TTS] block from agent response, synthesize speech, and emit audio_content event. */
|
|
20855
21692
|
async processTTSResponse(responseText) {
|
|
20856
21693
|
const match = TTS_BLOCK_REGEX.exec(responseText);
|
|
20857
21694
|
if (!match?.[1]) {
|
|
@@ -20888,7 +21725,9 @@ ${result.text}` : result.text;
|
|
|
20888
21725
|
this.log.warn({ err }, "TTS synthesis failed, skipping");
|
|
20889
21726
|
}
|
|
20890
21727
|
}
|
|
20891
|
-
//
|
|
21728
|
+
// Sends a special prompt to the agent to generate a short session title.
|
|
21729
|
+
// The session emitter is paused (excluding non-agent_event emissions) so the naming
|
|
21730
|
+
// prompt's output is intercepted by a capture handler instead of being forwarded to adapters.
|
|
20892
21731
|
async autoName() {
|
|
20893
21732
|
let title = "";
|
|
20894
21733
|
const captureHandler = (event) => {
|
|
@@ -21047,6 +21886,7 @@ ${result.text}` : result.text;
|
|
|
21047
21886
|
this.applySpawnResponse(newAgent.initialSessionResponse, newAgent.agentCapabilities);
|
|
21048
21887
|
this.log.info({ from: this.agentSwitchHistory.at(-1).agentName, to: agentName }, "Agent switched");
|
|
21049
21888
|
}
|
|
21889
|
+
/** Tear down the session: reject pending permissions, clear queue, destroy agent subprocess. */
|
|
21050
21890
|
async destroy() {
|
|
21051
21891
|
this.log.info("Session destroyed");
|
|
21052
21892
|
if (this.permissionGate.isPending) {
|
|
@@ -21072,12 +21912,17 @@ var init_session_manager = __esm({
|
|
|
21072
21912
|
store;
|
|
21073
21913
|
eventBus;
|
|
21074
21914
|
middlewareChain;
|
|
21915
|
+
/**
|
|
21916
|
+
* Inject the EventBus after construction. Deferred because EventBus is created
|
|
21917
|
+
* after SessionManager during bootstrap, so it cannot be passed to the constructor.
|
|
21918
|
+
*/
|
|
21075
21919
|
setEventBus(eventBus) {
|
|
21076
21920
|
this.eventBus = eventBus;
|
|
21077
21921
|
}
|
|
21078
21922
|
constructor(store = null) {
|
|
21079
21923
|
this.store = store;
|
|
21080
21924
|
}
|
|
21925
|
+
/** Create a new session by spawning an agent and persisting the initial record. */
|
|
21081
21926
|
async createSession(channelId, agentName, workingDirectory, agentManager) {
|
|
21082
21927
|
const agentInstance = await agentManager.spawn(agentName, workingDirectory);
|
|
21083
21928
|
const session = new Session({
|
|
@@ -21105,9 +21950,11 @@ var init_session_manager = __esm({
|
|
|
21105
21950
|
}
|
|
21106
21951
|
return session;
|
|
21107
21952
|
}
|
|
21953
|
+
/** Look up a live session by its OpenACP session ID. */
|
|
21108
21954
|
getSession(sessionId) {
|
|
21109
21955
|
return this.sessions.get(sessionId);
|
|
21110
21956
|
}
|
|
21957
|
+
/** Look up a live session by adapter channel and thread ID (checks per-adapter threadIds map first, then legacy fields). */
|
|
21111
21958
|
getSessionByThread(channelId, threadId) {
|
|
21112
21959
|
for (const session of this.sessions.values()) {
|
|
21113
21960
|
const adapterThread = session.threadIds.get(channelId);
|
|
@@ -21118,6 +21965,7 @@ var init_session_manager = __esm({
|
|
|
21118
21965
|
}
|
|
21119
21966
|
return void 0;
|
|
21120
21967
|
}
|
|
21968
|
+
/** Look up a live session by the agent's internal session ID (assigned by the ACP subprocess). */
|
|
21121
21969
|
getSessionByAgentSessionId(agentSessionId) {
|
|
21122
21970
|
for (const session of this.sessions.values()) {
|
|
21123
21971
|
if (session.agentSessionId === agentSessionId) {
|
|
@@ -21126,18 +21974,26 @@ var init_session_manager = __esm({
|
|
|
21126
21974
|
}
|
|
21127
21975
|
return void 0;
|
|
21128
21976
|
}
|
|
21977
|
+
/** Look up the persisted SessionRecord by the agent's internal session ID. */
|
|
21129
21978
|
getRecordByAgentSessionId(agentSessionId) {
|
|
21130
21979
|
return this.store?.findByAgentSessionId(agentSessionId);
|
|
21131
21980
|
}
|
|
21981
|
+
/** Look up the persisted SessionRecord by channel and thread ID. */
|
|
21132
21982
|
getRecordByThread(channelId, threadId) {
|
|
21133
21983
|
return this.store?.findByPlatform(
|
|
21134
21984
|
channelId,
|
|
21135
21985
|
(p2) => String(p2.topicId) === threadId || p2.threadId === threadId
|
|
21136
21986
|
);
|
|
21137
21987
|
}
|
|
21988
|
+
/** Register a session that was created externally (e.g. restored from store on startup). */
|
|
21138
21989
|
registerSession(session) {
|
|
21139
21990
|
this.sessions.set(session.id, session);
|
|
21140
21991
|
}
|
|
21992
|
+
/**
|
|
21993
|
+
* Merge a partial update into the stored SessionRecord. If no record exists yet and
|
|
21994
|
+
* the patch includes `sessionId`, it is treated as an initial save.
|
|
21995
|
+
* Pass `{ immediate: true }` to flush the store to disk synchronously.
|
|
21996
|
+
*/
|
|
21141
21997
|
async patchRecord(sessionId, patch, options) {
|
|
21142
21998
|
if (!this.store) return;
|
|
21143
21999
|
const record = this.store.get(sessionId);
|
|
@@ -21150,9 +22006,11 @@ var init_session_manager = __esm({
|
|
|
21150
22006
|
this.store.flush();
|
|
21151
22007
|
}
|
|
21152
22008
|
}
|
|
22009
|
+
/** Retrieve the persisted SessionRecord for a given session ID. Returns undefined if no store or record not found. */
|
|
21153
22010
|
getSessionRecord(sessionId) {
|
|
21154
22011
|
return this.store?.get(sessionId);
|
|
21155
22012
|
}
|
|
22013
|
+
/** Cancel a session: abort in-flight prompt, transition to cancelled, destroy agent, and persist. */
|
|
21156
22014
|
async cancelSession(sessionId) {
|
|
21157
22015
|
const session = this.sessions.get(sessionId);
|
|
21158
22016
|
if (session) {
|
|
@@ -21175,11 +22033,16 @@ var init_session_manager = __esm({
|
|
|
21175
22033
|
});
|
|
21176
22034
|
}
|
|
21177
22035
|
}
|
|
22036
|
+
/** List live (in-memory) sessions, optionally filtered by channel. Excludes assistant sessions. */
|
|
21178
22037
|
listSessions(channelId) {
|
|
21179
22038
|
const all = Array.from(this.sessions.values()).filter((s) => !s.isAssistant);
|
|
21180
22039
|
if (channelId) return all.filter((s) => s.channelId === channelId);
|
|
21181
22040
|
return all;
|
|
21182
22041
|
}
|
|
22042
|
+
/**
|
|
22043
|
+
* List all sessions (live + stored) as SessionSummary. Live sessions take precedence
|
|
22044
|
+
* over stored records — their real-time state (queueDepth, promptRunning) is used.
|
|
22045
|
+
*/
|
|
21183
22046
|
listAllSessions(channelId) {
|
|
21184
22047
|
if (this.store) {
|
|
21185
22048
|
let records = this.store.list().filter((r) => !r.isAssistant);
|
|
@@ -21241,6 +22104,7 @@ var init_session_manager = __esm({
|
|
|
21241
22104
|
isLive: true
|
|
21242
22105
|
}));
|
|
21243
22106
|
}
|
|
22107
|
+
/** List all stored SessionRecords, optionally filtered by status. Excludes assistant sessions. */
|
|
21244
22108
|
listRecords(filter) {
|
|
21245
22109
|
if (!this.store) return [];
|
|
21246
22110
|
let records = this.store.list().filter((r) => !r.isAssistant);
|
|
@@ -21249,6 +22113,7 @@ var init_session_manager = __esm({
|
|
|
21249
22113
|
}
|
|
21250
22114
|
return records;
|
|
21251
22115
|
}
|
|
22116
|
+
/** Remove a session's stored record and emit a SESSION_DELETED event. */
|
|
21252
22117
|
async removeRecord(sessionId) {
|
|
21253
22118
|
if (!this.store) return;
|
|
21254
22119
|
await this.store.remove(sessionId);
|
|
@@ -21359,7 +22224,10 @@ var init_session_bridge = __esm({
|
|
|
21359
22224
|
log34.error({ err, sessionId }, "Error in sendMessage middleware");
|
|
21360
22225
|
}
|
|
21361
22226
|
}
|
|
21362
|
-
/**
|
|
22227
|
+
/**
|
|
22228
|
+
* Determine if this bridge should forward the given event based on turn routing.
|
|
22229
|
+
* System events are always forwarded; turn events are routed only to the target adapter.
|
|
22230
|
+
*/
|
|
21363
22231
|
shouldForward(event) {
|
|
21364
22232
|
if (isSystemEvent(event)) return true;
|
|
21365
22233
|
const ctx = this.session.activeTurnContext;
|
|
@@ -21368,6 +22236,13 @@ var init_session_bridge = __esm({
|
|
|
21368
22236
|
if (target === null) return false;
|
|
21369
22237
|
return this.adapterId === target;
|
|
21370
22238
|
}
|
|
22239
|
+
/**
|
|
22240
|
+
* Subscribe to session events and start forwarding them to the adapter.
|
|
22241
|
+
*
|
|
22242
|
+
* Wires: agent events → adapter dispatch, permission UI, lifecycle persistence
|
|
22243
|
+
* (status changes, naming, prompt count), and EventBus notifications.
|
|
22244
|
+
* Also replays any commands or config options that arrived before the bridge connected.
|
|
22245
|
+
*/
|
|
21371
22246
|
connect() {
|
|
21372
22247
|
if (this.connected) return;
|
|
21373
22248
|
this.connected = true;
|
|
@@ -21444,6 +22319,7 @@ var init_session_bridge = __esm({
|
|
|
21444
22319
|
this.session.emit(SessionEv.AGENT_EVENT, { type: "config_option_update", options: this.session.configOptions });
|
|
21445
22320
|
}
|
|
21446
22321
|
}
|
|
22322
|
+
/** Unsubscribe all session event listeners and clean up adapter state. */
|
|
21447
22323
|
disconnect() {
|
|
21448
22324
|
if (!this.connected) return;
|
|
21449
22325
|
this.connected = false;
|
|
@@ -21976,6 +22852,12 @@ var init_message_transformer = __esm({
|
|
|
21976
22852
|
constructor(tunnelService) {
|
|
21977
22853
|
this.tunnelService = tunnelService;
|
|
21978
22854
|
}
|
|
22855
|
+
/**
|
|
22856
|
+
* Convert an agent event to an outgoing message for adapter delivery.
|
|
22857
|
+
*
|
|
22858
|
+
* For tool events, enriches the metadata with diff stats and viewer links
|
|
22859
|
+
* when a tunnel service is available.
|
|
22860
|
+
*/
|
|
21979
22861
|
transform(event, sessionContext) {
|
|
21980
22862
|
switch (event.type) {
|
|
21981
22863
|
case "text":
|
|
@@ -22250,6 +23132,11 @@ var init_session_store = __esm({
|
|
|
22250
23132
|
}
|
|
22251
23133
|
return void 0;
|
|
22252
23134
|
}
|
|
23135
|
+
/**
|
|
23136
|
+
* Find a session by its ACP agent session ID.
|
|
23137
|
+
* Checks current, original, and historical agent session IDs (from agent switches)
|
|
23138
|
+
* since the agent session ID changes on each switch.
|
|
23139
|
+
*/
|
|
22253
23140
|
findByAgentSessionId(agentSessionId) {
|
|
22254
23141
|
for (const record of this.records.values()) {
|
|
22255
23142
|
if (record.agentSessionId === agentSessionId || record.originalAgentSessionId === agentSessionId) {
|
|
@@ -22294,6 +23181,7 @@ var init_session_store = __esm({
|
|
|
22294
23181
|
if (!fs42.existsSync(dir)) fs42.mkdirSync(dir, { recursive: true });
|
|
22295
23182
|
fs42.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
22296
23183
|
}
|
|
23184
|
+
/** Clean up timers and process listeners. Call on shutdown to prevent leaks. */
|
|
22297
23185
|
destroy() {
|
|
22298
23186
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
22299
23187
|
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
|
|
@@ -22329,7 +23217,11 @@ var init_session_store = __esm({
|
|
|
22329
23217
|
}
|
|
22330
23218
|
}
|
|
22331
23219
|
}
|
|
22332
|
-
/**
|
|
23220
|
+
/**
|
|
23221
|
+
* Migrate old SessionRecord format to new multi-adapter format.
|
|
23222
|
+
* Converts single-adapter `platform` field to per-adapter `platforms` map,
|
|
23223
|
+
* and initializes `attachedAdapters` for records created before multi-adapter support.
|
|
23224
|
+
*/
|
|
22333
23225
|
migrateRecord(record) {
|
|
22334
23226
|
if (!record.platforms && record.platform && typeof record.platform === "object") {
|
|
22335
23227
|
const platformData = record.platform;
|
|
@@ -22342,6 +23234,7 @@ var init_session_store = __esm({
|
|
|
22342
23234
|
}
|
|
22343
23235
|
return record;
|
|
22344
23236
|
}
|
|
23237
|
+
/** Remove expired session records (past TTL). Active and assistant sessions are preserved. */
|
|
22345
23238
|
cleanup() {
|
|
22346
23239
|
const cutoff = Date.now() - this.ttlDays * 24 * 60 * 60 * 1e3;
|
|
22347
23240
|
let removed = 0;
|
|
@@ -22412,6 +23305,10 @@ var init_session_factory = __esm({
|
|
|
22412
23305
|
get speechService() {
|
|
22413
23306
|
return typeof this.speechServiceAccessor === "function" ? this.speechServiceAccessor() : this.speechServiceAccessor;
|
|
22414
23307
|
}
|
|
23308
|
+
/**
|
|
23309
|
+
* Create a new Session: spawn agent → create Session instance → hydrate ACP state → register.
|
|
23310
|
+
* Runs session:beforeCreate middleware (which can modify params or block creation).
|
|
23311
|
+
*/
|
|
22415
23312
|
async create(params) {
|
|
22416
23313
|
let createParams = params;
|
|
22417
23314
|
if (this.middlewareChain) {
|
|
@@ -22595,6 +23492,11 @@ var init_session_factory = __esm({
|
|
|
22595
23492
|
this.resumeLocks.set(sessionId, resumePromise);
|
|
22596
23493
|
return resumePromise;
|
|
22597
23494
|
}
|
|
23495
|
+
/**
|
|
23496
|
+
* Attempt to resume a session from disk when a message arrives on a thread with
|
|
23497
|
+
* no live session. Deduplicates concurrent resume attempts for the same thread
|
|
23498
|
+
* via resumeLocks to avoid spawning multiple agents.
|
|
23499
|
+
*/
|
|
22598
23500
|
async lazyResume(channelId, threadId) {
|
|
22599
23501
|
const store = this.sessionStore;
|
|
22600
23502
|
if (!store || !this.createFullSession) return null;
|
|
@@ -22698,6 +23600,7 @@ var init_session_factory = __esm({
|
|
|
22698
23600
|
this.resumeLocks.set(lockKey, resumePromise);
|
|
22699
23601
|
return resumePromise;
|
|
22700
23602
|
}
|
|
23603
|
+
/** Create a brand-new session, resolving agent name and workspace from config if not provided. */
|
|
22701
23604
|
async handleNewSession(channelId, agentName, workspacePath, options) {
|
|
22702
23605
|
if (!this.configManager || !this.agentCatalog || !this.createFullSession) {
|
|
22703
23606
|
throw new Error("SessionFactory not fully initialized");
|
|
@@ -22742,6 +23645,7 @@ var init_session_factory = __esm({
|
|
|
22742
23645
|
record.workingDir
|
|
22743
23646
|
);
|
|
22744
23647
|
}
|
|
23648
|
+
/** Create a session and inject conversation context from a ContextProvider (e.g., history from a previous session). */
|
|
22745
23649
|
async createSessionWithContext(params) {
|
|
22746
23650
|
if (!this.createFullSession) throw new Error("SessionFactory not fully initialized");
|
|
22747
23651
|
let contextResult = null;
|
|
@@ -22768,6 +23672,7 @@ var init_session_factory = __esm({
|
|
|
22768
23672
|
}
|
|
22769
23673
|
return { session, contextResult };
|
|
22770
23674
|
}
|
|
23675
|
+
/** Wire session-level side effects: usage tracking (via EventBus) and tunnel cleanup on session end. */
|
|
22771
23676
|
wireSideEffects(session, deps) {
|
|
22772
23677
|
session.on(SessionEv.AGENT_EVENT, (event) => {
|
|
22773
23678
|
if (event.type !== "usage") return;
|
|
@@ -22814,7 +23719,12 @@ var init_agent_switch_handler = __esm({
|
|
|
22814
23719
|
constructor(deps) {
|
|
22815
23720
|
this.deps = deps;
|
|
22816
23721
|
}
|
|
23722
|
+
/** Prevents concurrent switch operations on the same session */
|
|
22817
23723
|
switchingLocks = /* @__PURE__ */ new Set();
|
|
23724
|
+
/**
|
|
23725
|
+
* Switch a session to a different agent. Returns whether the previous
|
|
23726
|
+
* agent session was resumed or a new one was spawned.
|
|
23727
|
+
*/
|
|
22818
23728
|
async switch(sessionId, toAgent) {
|
|
22819
23729
|
if (this.switchingLocks.has(sessionId)) {
|
|
22820
23730
|
throw new Error("Switch already in progress");
|
|
@@ -23025,20 +23935,29 @@ var init_agent_catalog = __esm({
|
|
|
23025
23935
|
DEFAULT_TTL_HOURS = 24;
|
|
23026
23936
|
AgentCatalog = class {
|
|
23027
23937
|
store;
|
|
23938
|
+
/** Agents available in the remote registry (cached in memory after load). */
|
|
23028
23939
|
registryAgents = [];
|
|
23029
23940
|
cachePath;
|
|
23941
|
+
/** Directory where binary agent archives are extracted to. */
|
|
23030
23942
|
agentsDir;
|
|
23031
23943
|
constructor(store, cachePath, agentsDir) {
|
|
23032
23944
|
this.store = store;
|
|
23033
23945
|
this.cachePath = cachePath;
|
|
23034
23946
|
this.agentsDir = agentsDir;
|
|
23035
23947
|
}
|
|
23948
|
+
/**
|
|
23949
|
+
* Load installed agents from disk and hydrate the registry from cache/snapshot.
|
|
23950
|
+
*
|
|
23951
|
+
* Also enriches installed agents with registry metadata — fixes agents that
|
|
23952
|
+
* were migrated from older config formats with incomplete data.
|
|
23953
|
+
*/
|
|
23036
23954
|
load() {
|
|
23037
23955
|
this.store.load();
|
|
23038
23956
|
this.loadRegistryFromCacheOrSnapshot();
|
|
23039
23957
|
this.enrichInstalledFromRegistry();
|
|
23040
23958
|
}
|
|
23041
23959
|
// --- Registry ---
|
|
23960
|
+
/** Fetch the latest agent registry from the CDN and update the local cache. */
|
|
23042
23961
|
async fetchRegistry() {
|
|
23043
23962
|
try {
|
|
23044
23963
|
log39.info("Fetching agent registry from CDN...");
|
|
@@ -23058,6 +23977,7 @@ var init_agent_catalog = __esm({
|
|
|
23058
23977
|
log39.warn({ err }, "Failed to fetch registry, using cached data");
|
|
23059
23978
|
}
|
|
23060
23979
|
}
|
|
23980
|
+
/** Re-fetch registry only if the local cache has expired (24-hour TTL). */
|
|
23061
23981
|
async refreshRegistryIfStale() {
|
|
23062
23982
|
if (this.isCacheStale()) {
|
|
23063
23983
|
await this.fetchRegistry();
|
|
@@ -23069,6 +23989,7 @@ var init_agent_catalog = __esm({
|
|
|
23069
23989
|
getRegistryAgent(registryId) {
|
|
23070
23990
|
return this.registryAgents.find((a) => a.id === registryId);
|
|
23071
23991
|
}
|
|
23992
|
+
/** Find a registry agent by registry ID or by its short alias (e.g., "claude"). */
|
|
23072
23993
|
findRegistryAgent(keyOrId) {
|
|
23073
23994
|
const byId = this.registryAgents.find((a) => a.id === keyOrId);
|
|
23074
23995
|
if (byId) return byId;
|
|
@@ -23085,6 +24006,15 @@ var init_agent_catalog = __esm({
|
|
|
23085
24006
|
return this.store.getAgent(key);
|
|
23086
24007
|
}
|
|
23087
24008
|
// --- Discovery ---
|
|
24009
|
+
/**
|
|
24010
|
+
* Build the unified list of all agents (installed + registry-only).
|
|
24011
|
+
*
|
|
24012
|
+
* Installed agents appear first with their live availability status.
|
|
24013
|
+
* Registry agents that aren't installed yet show whether a distribution
|
|
24014
|
+
* exists for the current platform. Missing external dependencies
|
|
24015
|
+
* (e.g., claude CLI) are surfaced as `missingDeps` for UI display
|
|
24016
|
+
* but do NOT block installation.
|
|
24017
|
+
*/
|
|
23088
24018
|
getAvailable() {
|
|
23089
24019
|
const installed = this.getInstalledEntries();
|
|
23090
24020
|
const items = [];
|
|
@@ -23125,6 +24055,7 @@ var init_agent_catalog = __esm({
|
|
|
23125
24055
|
}
|
|
23126
24056
|
return items;
|
|
23127
24057
|
}
|
|
24058
|
+
/** Check if an agent can be installed on this system (platform + dependencies). */
|
|
23128
24059
|
checkAvailability(keyOrId) {
|
|
23129
24060
|
const agent = this.findRegistryAgent(keyOrId);
|
|
23130
24061
|
if (!agent) return { available: false, reason: "Not found in the agent registry." };
|
|
@@ -23135,6 +24066,12 @@ var init_agent_catalog = __esm({
|
|
|
23135
24066
|
return checkDependencies(agent.id);
|
|
23136
24067
|
}
|
|
23137
24068
|
// --- Install/Uninstall ---
|
|
24069
|
+
/**
|
|
24070
|
+
* Install an agent from the registry.
|
|
24071
|
+
*
|
|
24072
|
+
* Resolves the distribution (npx/uvx/binary), downloads binary archives
|
|
24073
|
+
* if needed, and persists the agent definition in the store.
|
|
24074
|
+
*/
|
|
23138
24075
|
async install(keyOrId, progress, force) {
|
|
23139
24076
|
const agent = this.findRegistryAgent(keyOrId);
|
|
23140
24077
|
if (!agent) {
|
|
@@ -23159,6 +24096,7 @@ var init_agent_catalog = __esm({
|
|
|
23159
24096
|
registerFallbackAgent(key, data) {
|
|
23160
24097
|
this.store.addAgent(key, data);
|
|
23161
24098
|
}
|
|
24099
|
+
/** Remove an installed agent and delete its binary directory if applicable. */
|
|
23162
24100
|
async uninstall(key) {
|
|
23163
24101
|
if (this.store.hasAgent(key)) {
|
|
23164
24102
|
await uninstallAgent(key, this.store);
|
|
@@ -23167,6 +24105,7 @@ var init_agent_catalog = __esm({
|
|
|
23167
24105
|
return { ok: false, error: `"${key}" is not installed.` };
|
|
23168
24106
|
}
|
|
23169
24107
|
// --- Resolution (for AgentManager) ---
|
|
24108
|
+
/** Convert an installed agent's short key to an AgentDefinition for spawning. */
|
|
23170
24109
|
resolve(key) {
|
|
23171
24110
|
const agent = this.store.getAgent(key);
|
|
23172
24111
|
if (!agent) return void 0;
|
|
@@ -23310,6 +24249,7 @@ var init_agent_store = __esm({
|
|
|
23310
24249
|
constructor(filePath) {
|
|
23311
24250
|
this.filePath = filePath;
|
|
23312
24251
|
}
|
|
24252
|
+
/** Load and validate the store from disk. Starts fresh if file is missing or invalid. */
|
|
23313
24253
|
load() {
|
|
23314
24254
|
if (!fs44.existsSync(this.filePath)) {
|
|
23315
24255
|
this.data = { version: 1, installed: {} };
|
|
@@ -23349,6 +24289,11 @@ var init_agent_store = __esm({
|
|
|
23349
24289
|
hasAgent(key) {
|
|
23350
24290
|
return key in this.data.installed;
|
|
23351
24291
|
}
|
|
24292
|
+
/**
|
|
24293
|
+
* Persist the store to disk using atomic write (write to .tmp, then rename).
|
|
24294
|
+
* File permissions are restricted to owner-only (0o600) since the store
|
|
24295
|
+
* may contain agent binary paths and environment variables.
|
|
24296
|
+
*/
|
|
23352
24297
|
save() {
|
|
23353
24298
|
fs44.mkdirSync(path49.dirname(this.filePath), { recursive: true });
|
|
23354
24299
|
const tmpPath = this.filePath + ".tmp";
|
|
@@ -23446,6 +24391,10 @@ var init_service_registry = __esm({
|
|
|
23446
24391
|
"use strict";
|
|
23447
24392
|
ServiceRegistry = class {
|
|
23448
24393
|
services = /* @__PURE__ */ new Map();
|
|
24394
|
+
/**
|
|
24395
|
+
* Register a service. Throws if the service name is already taken.
|
|
24396
|
+
* Use `registerOverride` to intentionally replace an existing service.
|
|
24397
|
+
*/
|
|
23449
24398
|
register(name, implementation, pluginName) {
|
|
23450
24399
|
if (this.services.has(name)) {
|
|
23451
24400
|
const existing = this.services.get(name);
|
|
@@ -23453,21 +24402,27 @@ var init_service_registry = __esm({
|
|
|
23453
24402
|
}
|
|
23454
24403
|
this.services.set(name, { implementation, pluginName });
|
|
23455
24404
|
}
|
|
24405
|
+
/** Register a service, replacing any existing registration (used by override plugins). */
|
|
23456
24406
|
registerOverride(name, implementation, pluginName) {
|
|
23457
24407
|
this.services.set(name, { implementation, pluginName });
|
|
23458
24408
|
}
|
|
24409
|
+
/** Retrieve a service by name. Returns undefined if not registered. */
|
|
23459
24410
|
get(name) {
|
|
23460
24411
|
return this.services.get(name)?.implementation;
|
|
23461
24412
|
}
|
|
24413
|
+
/** Check whether a service is registered. */
|
|
23462
24414
|
has(name) {
|
|
23463
24415
|
return this.services.has(name);
|
|
23464
24416
|
}
|
|
24417
|
+
/** List all registered services with their owning plugin names. */
|
|
23465
24418
|
list() {
|
|
23466
24419
|
return [...this.services.entries()].map(([name, { pluginName }]) => ({ name, pluginName }));
|
|
23467
24420
|
}
|
|
24421
|
+
/** Remove a single service by name. */
|
|
23468
24422
|
unregister(name) {
|
|
23469
24423
|
this.services.delete(name);
|
|
23470
24424
|
}
|
|
24425
|
+
/** Remove all services owned by a specific plugin (called during plugin unload). */
|
|
23471
24426
|
unregisterByPlugin(pluginName) {
|
|
23472
24427
|
for (const [name, entry] of this.services) {
|
|
23473
24428
|
if (entry.pluginName === pluginName) {
|
|
@@ -23489,6 +24444,7 @@ var init_middleware_chain = __esm({
|
|
|
23489
24444
|
chains = /* @__PURE__ */ new Map();
|
|
23490
24445
|
errorHandler;
|
|
23491
24446
|
errorTracker;
|
|
24447
|
+
/** Register a middleware handler for a hook. Handlers are kept sorted by priority. */
|
|
23492
24448
|
add(hook, pluginName, opts) {
|
|
23493
24449
|
const entry = {
|
|
23494
24450
|
pluginName,
|
|
@@ -23503,6 +24459,15 @@ var init_middleware_chain = __esm({
|
|
|
23503
24459
|
this.chains.set(hook, [entry]);
|
|
23504
24460
|
}
|
|
23505
24461
|
}
|
|
24462
|
+
/**
|
|
24463
|
+
* Execute the middleware chain for a hook, ending with the core handler.
|
|
24464
|
+
*
|
|
24465
|
+
* The chain is built recursively: each handler calls `next()` to invoke the
|
|
24466
|
+
* next handler, with the core handler at the end. If no middleware is registered,
|
|
24467
|
+
* the core handler runs directly.
|
|
24468
|
+
*
|
|
24469
|
+
* @returns The final payload, or `null` if any handler short-circuited.
|
|
24470
|
+
*/
|
|
23506
24471
|
async execute(hook, payload, coreHandler) {
|
|
23507
24472
|
const handlers = this.chains.get(hook);
|
|
23508
24473
|
if (!handlers || handlers.length === 0) {
|
|
@@ -23572,6 +24537,7 @@ var init_middleware_chain = __esm({
|
|
|
23572
24537
|
const start = buildNext(0, payload);
|
|
23573
24538
|
return start();
|
|
23574
24539
|
}
|
|
24540
|
+
/** Remove all middleware handlers registered by a specific plugin. */
|
|
23575
24541
|
removeAll(pluginName) {
|
|
23576
24542
|
for (const [hook, handlers] of this.chains.entries()) {
|
|
23577
24543
|
const filtered = handlers.filter((h) => h.pluginName !== pluginName);
|
|
@@ -23582,9 +24548,11 @@ var init_middleware_chain = __esm({
|
|
|
23582
24548
|
}
|
|
23583
24549
|
}
|
|
23584
24550
|
}
|
|
24551
|
+
/** Set a callback for middleware errors (e.g., logging). */
|
|
23585
24552
|
setErrorHandler(fn) {
|
|
23586
24553
|
this.errorHandler = fn;
|
|
23587
24554
|
}
|
|
24555
|
+
/** Attach an ErrorTracker for circuit-breaking misbehaving plugins. */
|
|
23588
24556
|
setErrorTracker(tracker) {
|
|
23589
24557
|
this.errorTracker = tracker;
|
|
23590
24558
|
}
|
|
@@ -23602,10 +24570,15 @@ var init_error_tracker = __esm({
|
|
|
23602
24570
|
disabled = /* @__PURE__ */ new Set();
|
|
23603
24571
|
exempt = /* @__PURE__ */ new Set();
|
|
23604
24572
|
config;
|
|
24573
|
+
/** Callback fired when a plugin is auto-disabled due to error budget exhaustion. */
|
|
23605
24574
|
onDisabled;
|
|
23606
24575
|
constructor(config) {
|
|
23607
24576
|
this.config = { maxErrors: config?.maxErrors ?? 10, windowMs: config?.windowMs ?? 36e5 };
|
|
23608
24577
|
}
|
|
24578
|
+
/**
|
|
24579
|
+
* Record an error for a plugin. If the error budget is exceeded,
|
|
24580
|
+
* the plugin is disabled and the `onDisabled` callback fires.
|
|
24581
|
+
*/
|
|
23609
24582
|
increment(pluginName) {
|
|
23610
24583
|
if (this.exempt.has(pluginName)) return;
|
|
23611
24584
|
const now = Date.now();
|
|
@@ -23622,13 +24595,16 @@ var init_error_tracker = __esm({
|
|
|
23622
24595
|
this.onDisabled?.(pluginName, reason);
|
|
23623
24596
|
}
|
|
23624
24597
|
}
|
|
24598
|
+
/** Check if a plugin has been disabled due to errors. */
|
|
23625
24599
|
isDisabled(pluginName) {
|
|
23626
24600
|
return this.disabled.has(pluginName);
|
|
23627
24601
|
}
|
|
24602
|
+
/** Re-enable a plugin and clear its error history. */
|
|
23628
24603
|
reset(pluginName) {
|
|
23629
24604
|
this.disabled.delete(pluginName);
|
|
23630
24605
|
this.errors.delete(pluginName);
|
|
23631
24606
|
}
|
|
24607
|
+
/** Mark a plugin as exempt from circuit-breaking (e.g., essential plugins). */
|
|
23632
24608
|
setExempt(pluginName) {
|
|
23633
24609
|
this.exempt.add(pluginName);
|
|
23634
24610
|
}
|
|
@@ -23646,6 +24622,7 @@ var init_plugin_storage = __esm({
|
|
|
23646
24622
|
PluginStorageImpl = class {
|
|
23647
24623
|
kvPath;
|
|
23648
24624
|
dataDir;
|
|
24625
|
+
/** Serializes writes to prevent concurrent file corruption */
|
|
23649
24626
|
writeChain = Promise.resolve();
|
|
23650
24627
|
constructor(baseDir) {
|
|
23651
24628
|
this.dataDir = path50.join(baseDir, "data");
|
|
@@ -23686,6 +24663,7 @@ var init_plugin_storage = __esm({
|
|
|
23686
24663
|
async list() {
|
|
23687
24664
|
return Object.keys(this.readKv());
|
|
23688
24665
|
}
|
|
24666
|
+
/** Returns the plugin's data directory, creating it lazily on first access. */
|
|
23689
24667
|
getDataDir() {
|
|
23690
24668
|
fs45.mkdirSync(this.dataDir, { recursive: true });
|
|
23691
24669
|
return this.dataDir;
|
|
@@ -23861,6 +24839,11 @@ function createPluginContext(opts) {
|
|
|
23861
24839
|
return core;
|
|
23862
24840
|
},
|
|
23863
24841
|
instanceRoot,
|
|
24842
|
+
/**
|
|
24843
|
+
* Called by LifecycleManager during plugin teardown.
|
|
24844
|
+
* Unregisters all event handlers, middleware, commands, and services
|
|
24845
|
+
* registered by this plugin, preventing leaks across reloads.
|
|
24846
|
+
*/
|
|
23864
24847
|
cleanup() {
|
|
23865
24848
|
for (const { event, handler } of registeredListeners) {
|
|
23866
24849
|
eventBus.off(event, handler);
|
|
@@ -23967,12 +24950,15 @@ var init_lifecycle_manager = __esm({
|
|
|
23967
24950
|
loadOrder = [];
|
|
23968
24951
|
_loaded = /* @__PURE__ */ new Set();
|
|
23969
24952
|
_failed = /* @__PURE__ */ new Set();
|
|
24953
|
+
/** Names of plugins that successfully completed setup(). */
|
|
23970
24954
|
get loadedPlugins() {
|
|
23971
24955
|
return [...this._loaded];
|
|
23972
24956
|
}
|
|
24957
|
+
/** Names of plugins whose setup() threw an error. These plugins are skipped but don't crash the system. */
|
|
23973
24958
|
get failedPlugins() {
|
|
23974
24959
|
return [...this._failed];
|
|
23975
24960
|
}
|
|
24961
|
+
/** The PluginRegistry tracking installed and enabled plugin state. */
|
|
23976
24962
|
get registry() {
|
|
23977
24963
|
return this.pluginRegistry;
|
|
23978
24964
|
}
|
|
@@ -24019,6 +25005,12 @@ var init_lifecycle_manager = __esm({
|
|
|
24019
25005
|
return this;
|
|
24020
25006
|
} };
|
|
24021
25007
|
}
|
|
25008
|
+
/**
|
|
25009
|
+
* Boot a set of plugins in dependency order.
|
|
25010
|
+
*
|
|
25011
|
+
* Can be called multiple times (e.g., core plugins first, then dev plugins later).
|
|
25012
|
+
* Already-loaded plugins are included in dependency resolution but not re-booted.
|
|
25013
|
+
*/
|
|
24022
25014
|
async boot(plugins) {
|
|
24023
25015
|
const newNames = new Set(plugins.map((p2) => p2.name));
|
|
24024
25016
|
const allForResolution = [...this.loadOrder.filter((p2) => !newNames.has(p2.name)), ...plugins];
|
|
@@ -24130,6 +25122,11 @@ var init_lifecycle_manager = __esm({
|
|
|
24130
25122
|
}
|
|
24131
25123
|
}
|
|
24132
25124
|
}
|
|
25125
|
+
/**
|
|
25126
|
+
* Unload a single plugin: call teardown(), clean up its context
|
|
25127
|
+
* (listeners, middleware, services), and remove from tracked state.
|
|
25128
|
+
* Used for hot-reload: unload → rebuild → re-boot.
|
|
25129
|
+
*/
|
|
24133
25130
|
async unloadPlugin(name) {
|
|
24134
25131
|
if (!this._loaded.has(name)) return;
|
|
24135
25132
|
const plugin2 = this.loadOrder.find((p2) => p2.name === name);
|
|
@@ -24149,6 +25146,10 @@ var init_lifecycle_manager = __esm({
|
|
|
24149
25146
|
this.loadOrder = this.loadOrder.filter((p2) => p2.name !== name);
|
|
24150
25147
|
this.eventBus?.emit(BusEvent.PLUGIN_UNLOADED, { name });
|
|
24151
25148
|
}
|
|
25149
|
+
/**
|
|
25150
|
+
* Gracefully shut down all loaded plugins.
|
|
25151
|
+
* Teardown runs in reverse boot order so that dependencies outlive their dependents.
|
|
25152
|
+
*/
|
|
24152
25153
|
async shutdown() {
|
|
24153
25154
|
const reversed = [...this.loadOrder].reverse();
|
|
24154
25155
|
for (const plugin2 of reversed) {
|
|
@@ -24182,16 +25183,19 @@ var init_menu_registry = __esm({
|
|
|
24182
25183
|
log41 = createChildLogger({ module: "menu-registry" });
|
|
24183
25184
|
MenuRegistry = class {
|
|
24184
25185
|
items = /* @__PURE__ */ new Map();
|
|
25186
|
+
/** Register or replace a menu item by its unique ID. */
|
|
24185
25187
|
register(item) {
|
|
24186
25188
|
this.items.set(item.id, item);
|
|
24187
25189
|
}
|
|
25190
|
+
/** Remove a menu item by ID. */
|
|
24188
25191
|
unregister(id) {
|
|
24189
25192
|
this.items.delete(id);
|
|
24190
25193
|
}
|
|
25194
|
+
/** Look up a single menu item by ID. */
|
|
24191
25195
|
getItem(id) {
|
|
24192
25196
|
return this.items.get(id);
|
|
24193
25197
|
}
|
|
24194
|
-
/** Get all visible items sorted by priority */
|
|
25198
|
+
/** Get all visible items sorted by priority (lower number = shown first). */
|
|
24195
25199
|
getItems() {
|
|
24196
25200
|
return [...this.items.values()].filter((item) => {
|
|
24197
25201
|
if (!item.visible) return true;
|
|
@@ -24281,19 +25285,31 @@ var init_assistant_registry = __esm({
|
|
|
24281
25285
|
AssistantRegistry = class {
|
|
24282
25286
|
sections = /* @__PURE__ */ new Map();
|
|
24283
25287
|
_instanceRoot = "";
|
|
24284
|
-
/** Set the instance root path used in assistant guidelines */
|
|
25288
|
+
/** Set the instance root path used in assistant guidelines. */
|
|
24285
25289
|
setInstanceRoot(root) {
|
|
24286
25290
|
this._instanceRoot = root;
|
|
24287
25291
|
}
|
|
25292
|
+
/** Register a prompt section. Overwrites any existing section with the same id. */
|
|
24288
25293
|
register(section) {
|
|
24289
25294
|
if (this.sections.has(section.id)) {
|
|
24290
25295
|
log42.warn({ id: section.id }, "Assistant section overwritten");
|
|
24291
25296
|
}
|
|
24292
25297
|
this.sections.set(section.id, section);
|
|
24293
25298
|
}
|
|
25299
|
+
/** Remove a previously registered section by id. */
|
|
24294
25300
|
unregister(id) {
|
|
24295
25301
|
this.sections.delete(id);
|
|
24296
25302
|
}
|
|
25303
|
+
/**
|
|
25304
|
+
* Compose the full system prompt from all registered sections.
|
|
25305
|
+
*
|
|
25306
|
+
* Sections are sorted by priority (ascending), each contributing a titled
|
|
25307
|
+
* markdown block. If a section's `buildContext()` throws, it is skipped
|
|
25308
|
+
* gracefully so one broken section doesn't break the entire prompt.
|
|
25309
|
+
*
|
|
25310
|
+
* If `channelId` is provided, a "Current Channel" block is injected at the
|
|
25311
|
+
* top of the prompt so the assistant can adapt its behavior to the platform.
|
|
25312
|
+
*/
|
|
24297
25313
|
buildSystemPrompt(channelId) {
|
|
24298
25314
|
const sorted = [...this.sections.values()].sort((a, b) => a.priority - b.priority);
|
|
24299
25315
|
const parts = [ASSISTANT_PREAMBLE];
|
|
@@ -24336,6 +25352,13 @@ var init_assistant_manager = __esm({
|
|
|
24336
25352
|
}
|
|
24337
25353
|
sessions = /* @__PURE__ */ new Map();
|
|
24338
25354
|
pendingSystemPrompts = /* @__PURE__ */ new Map();
|
|
25355
|
+
/**
|
|
25356
|
+
* Returns the assistant session for a channel, creating one if needed.
|
|
25357
|
+
*
|
|
25358
|
+
* If a persisted assistant session exists in the store, it is reused
|
|
25359
|
+
* (same session ID) to preserve conversation history. The system prompt
|
|
25360
|
+
* is always rebuilt fresh and deferred until the first user message.
|
|
25361
|
+
*/
|
|
24339
25362
|
async getOrSpawn(channelId, threadId) {
|
|
24340
25363
|
const existing = this.core.sessionStore?.findAssistant(channelId);
|
|
24341
25364
|
const session = await this.core.createSession({
|
|
@@ -24356,6 +25379,7 @@ var init_assistant_manager = __esm({
|
|
|
24356
25379
|
);
|
|
24357
25380
|
return session;
|
|
24358
25381
|
}
|
|
25382
|
+
/** Returns the active assistant session for a channel, or null if none exists. */
|
|
24359
25383
|
get(channelId) {
|
|
24360
25384
|
return this.sessions.get(channelId) ?? null;
|
|
24361
25385
|
}
|
|
@@ -24368,6 +25392,7 @@ var init_assistant_manager = __esm({
|
|
|
24368
25392
|
if (prompt) this.pendingSystemPrompts.delete(channelId);
|
|
24369
25393
|
return prompt;
|
|
24370
25394
|
}
|
|
25395
|
+
/** Checks whether a given session ID belongs to the built-in assistant. */
|
|
24371
25396
|
isAssistant(sessionId) {
|
|
24372
25397
|
for (const s of this.sessions.values()) {
|
|
24373
25398
|
if (s.id === sessionId) return true;
|
|
@@ -24666,30 +25691,49 @@ var init_core = __esm({
|
|
|
24666
25691
|
menuRegistry = new MenuRegistry();
|
|
24667
25692
|
assistantRegistry = new AssistantRegistry();
|
|
24668
25693
|
assistantManager;
|
|
24669
|
-
//
|
|
25694
|
+
// Services (security, notifications, speech, etc.) are provided by plugins that
|
|
25695
|
+
// register during boot. Core accesses them lazily via ServiceRegistry so it doesn't
|
|
25696
|
+
// need compile-time dependencies on plugin implementations.
|
|
25697
|
+
/** @throws if the service hasn't been registered by its plugin yet */
|
|
24670
25698
|
getService(name) {
|
|
24671
25699
|
const svc = this.lifecycleManager.serviceRegistry.get(name);
|
|
24672
25700
|
if (!svc) throw new Error(`Service '${name}' not registered \u2014 is the ${name} plugin loaded?`);
|
|
24673
25701
|
return svc;
|
|
24674
25702
|
}
|
|
25703
|
+
/** Access control and rate-limiting guard (provided by security plugin). */
|
|
24675
25704
|
get securityGuard() {
|
|
24676
25705
|
return this.getService("security");
|
|
24677
25706
|
}
|
|
25707
|
+
/** Cross-session notification delivery (provided by notifications plugin). */
|
|
24678
25708
|
get notificationManager() {
|
|
24679
25709
|
return this.getService("notifications");
|
|
24680
25710
|
}
|
|
25711
|
+
/** File I/O service for agent attachment storage (provided by file-service plugin). */
|
|
24681
25712
|
get fileService() {
|
|
24682
25713
|
return this.getService("file-service");
|
|
24683
25714
|
}
|
|
25715
|
+
/** Text-to-speech / speech-to-text engine (provided by speech plugin). */
|
|
24684
25716
|
get speechService() {
|
|
24685
25717
|
return this.getService("speech");
|
|
24686
25718
|
}
|
|
25719
|
+
/** Conversation history builder for context injection (provided by context plugin). */
|
|
24687
25720
|
get contextManager() {
|
|
24688
25721
|
return this.getService("context");
|
|
24689
25722
|
}
|
|
25723
|
+
/** Per-plugin persistent settings (e.g. API keys). */
|
|
24690
25724
|
get settingsManager() {
|
|
24691
25725
|
return this.lifecycleManager.settingsManager;
|
|
24692
25726
|
}
|
|
25727
|
+
/**
|
|
25728
|
+
* Bootstrap all core subsystems. The boot order matters:
|
|
25729
|
+
* 1. AgentCatalog + AgentManager (agent definitions)
|
|
25730
|
+
* 2. SessionStore + SessionManager (session persistence and lookup)
|
|
25731
|
+
* 3. EventBus (inter-module communication)
|
|
25732
|
+
* 4. SessionFactory (session creation pipeline)
|
|
25733
|
+
* 5. LifecycleManager (plugin infrastructure)
|
|
25734
|
+
* 6. Wire middleware chain into factory + manager
|
|
25735
|
+
* 7. AgentSwitchHandler, config listeners, menu/assistant registries
|
|
25736
|
+
*/
|
|
24693
25737
|
constructor(configManager, ctx) {
|
|
24694
25738
|
this.configManager = configManager;
|
|
24695
25739
|
this.instanceContext = ctx;
|
|
@@ -24800,16 +25844,28 @@ var init_core = __esm({
|
|
|
24800
25844
|
this.lifecycleManager.serviceRegistry.register("menu-registry", this.menuRegistry, "core");
|
|
24801
25845
|
this.lifecycleManager.serviceRegistry.register("assistant-registry", this.assistantRegistry, "core");
|
|
24802
25846
|
}
|
|
25847
|
+
/** Optional tunnel for generating public URLs (code viewer links, etc.). */
|
|
24803
25848
|
get tunnelService() {
|
|
24804
25849
|
return this._tunnelService;
|
|
24805
25850
|
}
|
|
25851
|
+
/** Propagate tunnel service to MessageTransformer so it can generate viewer links. */
|
|
24806
25852
|
set tunnelService(service) {
|
|
24807
25853
|
this._tunnelService = service;
|
|
24808
25854
|
this.messageTransformer.tunnelService = service;
|
|
24809
25855
|
}
|
|
25856
|
+
/**
|
|
25857
|
+
* Register a messaging adapter (e.g. Telegram, Slack, SSE).
|
|
25858
|
+
*
|
|
25859
|
+
* Adapters must be registered before `start()`. The adapter name serves as its
|
|
25860
|
+
* channel ID throughout the system — used in session records, bridge keys, and routing.
|
|
25861
|
+
*/
|
|
24810
25862
|
registerAdapter(name, adapter) {
|
|
24811
25863
|
this.adapters.set(name, adapter);
|
|
24812
25864
|
}
|
|
25865
|
+
/**
|
|
25866
|
+
* Start all registered adapters. Adapters that fail are logged but do not
|
|
25867
|
+
* prevent others from starting. Throws only if ALL adapters fail.
|
|
25868
|
+
*/
|
|
24813
25869
|
async start() {
|
|
24814
25870
|
this.agentCatalog.refreshRegistryIfStale().catch((err) => {
|
|
24815
25871
|
log44.warn({ err }, "Background registry refresh failed");
|
|
@@ -24829,6 +25885,10 @@ var init_core = __esm({
|
|
|
24829
25885
|
);
|
|
24830
25886
|
}
|
|
24831
25887
|
}
|
|
25888
|
+
/**
|
|
25889
|
+
* Graceful shutdown: notify users, persist session state, stop adapters.
|
|
25890
|
+
* Agent subprocesses are not explicitly killed — they exit with the parent process.
|
|
25891
|
+
*/
|
|
24832
25892
|
async stop() {
|
|
24833
25893
|
try {
|
|
24834
25894
|
const nm = this.lifecycleManager.serviceRegistry.get("notifications");
|
|
@@ -24847,6 +25907,13 @@ var init_core = __esm({
|
|
|
24847
25907
|
}
|
|
24848
25908
|
}
|
|
24849
25909
|
// --- Archive ---
|
|
25910
|
+
/**
|
|
25911
|
+
* Archive a session: delete its adapter topic/thread and cancel the session.
|
|
25912
|
+
*
|
|
25913
|
+
* Only sessions in archivable states (active, cancelled, error) can be archived —
|
|
25914
|
+
* initializing and finished sessions are excluded.
|
|
25915
|
+
* The adapter handles platform-side cleanup (e.g. deleting a Telegram topic).
|
|
25916
|
+
*/
|
|
24850
25917
|
async archiveSession(sessionId) {
|
|
24851
25918
|
const session = this.sessionManager.getSession(sessionId);
|
|
24852
25919
|
if (!session) return { ok: false, error: "Session not found (must be in memory)" };
|
|
@@ -24866,6 +25933,18 @@ var init_core = __esm({
|
|
|
24866
25933
|
}
|
|
24867
25934
|
}
|
|
24868
25935
|
// --- Message Routing ---
|
|
25936
|
+
/**
|
|
25937
|
+
* Route an incoming platform message to the appropriate session.
|
|
25938
|
+
*
|
|
25939
|
+
* Flow:
|
|
25940
|
+
* 1. Run `message:incoming` middleware (plugins can modify or block)
|
|
25941
|
+
* 2. SecurityGuard checks user access and per-user session limits
|
|
25942
|
+
* 3. Find session by channel+thread (in-memory lookup, then lazy resume from disk)
|
|
25943
|
+
* 4. For assistant sessions, prepend any deferred system prompt
|
|
25944
|
+
* 5. Emit `message:queued` for SSE clients, then enqueue the prompt on the session
|
|
25945
|
+
*
|
|
25946
|
+
* If no session is found, the user is told to start one with /new.
|
|
25947
|
+
*/
|
|
24869
25948
|
async handleMessage(message) {
|
|
24870
25949
|
log44.debug(
|
|
24871
25950
|
{
|
|
@@ -24947,6 +26026,17 @@ ${text5}`;
|
|
|
24947
26026
|
}
|
|
24948
26027
|
}
|
|
24949
26028
|
// --- Unified Session Creation Pipeline ---
|
|
26029
|
+
/**
|
|
26030
|
+
* Create (or resume) a session with full wiring: agent, adapter thread, bridge, persistence.
|
|
26031
|
+
*
|
|
26032
|
+
* This is the single entry point for session creation. The pipeline:
|
|
26033
|
+
* 1. SessionFactory spawns/resumes the agent process
|
|
26034
|
+
* 2. Adapter creates a thread/topic if requested
|
|
26035
|
+
* 3. Initial session record is persisted (so lazy resume can find it by threadId)
|
|
26036
|
+
* 4. SessionBridge connects agent events to the adapter
|
|
26037
|
+
* 5. For headless sessions (no adapter), fallback event handlers are wired inline
|
|
26038
|
+
* 6. Side effects (usage tracking, tunnel cleanup) are attached
|
|
26039
|
+
*/
|
|
24950
26040
|
async createSession(params) {
|
|
24951
26041
|
const session = await this.sessionFactory.create(params);
|
|
24952
26042
|
if (params.threadId) {
|
|
@@ -25069,9 +26159,16 @@ ${text5}`;
|
|
|
25069
26159
|
);
|
|
25070
26160
|
return session;
|
|
25071
26161
|
}
|
|
26162
|
+
/** Convenience wrapper: create a new session with default agent/workspace resolution. */
|
|
25072
26163
|
async handleNewSession(channelId, agentName, workspacePath, options) {
|
|
25073
26164
|
return this.sessionFactory.handleNewSession(channelId, agentName, workspacePath, options);
|
|
25074
26165
|
}
|
|
26166
|
+
/**
|
|
26167
|
+
* Adopt an externally-started agent session (e.g. from a CLI `openacp adopt` command).
|
|
26168
|
+
*
|
|
26169
|
+
* Validates that the agent supports resume, checks session limits, avoids duplicates,
|
|
26170
|
+
* then creates a full session via the unified pipeline with resume semantics.
|
|
26171
|
+
*/
|
|
25075
26172
|
async adoptSession(agentName, agentSessionId, cwd, channelId) {
|
|
25076
26173
|
const caps = getAgentCapabilities(agentName);
|
|
25077
26174
|
if (!caps.supportsResume) {
|
|
@@ -25182,22 +26279,33 @@ ${text5}`;
|
|
|
25182
26279
|
status: "adopted"
|
|
25183
26280
|
};
|
|
25184
26281
|
}
|
|
26282
|
+
/** Start a new chat within the same agent and workspace as the current session's thread. */
|
|
25185
26283
|
async handleNewChat(channelId, currentThreadId) {
|
|
25186
26284
|
return this.sessionFactory.handleNewChat(channelId, currentThreadId);
|
|
25187
26285
|
}
|
|
26286
|
+
/** Create a session and inject conversation context from a prior session or repo. */
|
|
25188
26287
|
async createSessionWithContext(params) {
|
|
25189
26288
|
return this.sessionFactory.createSessionWithContext(params);
|
|
25190
26289
|
}
|
|
25191
26290
|
// --- Agent Switch ---
|
|
26291
|
+
/** Switch a session's active agent. Delegates to AgentSwitchHandler for state coordination. */
|
|
25192
26292
|
async switchSessionAgent(sessionId, toAgent) {
|
|
25193
26293
|
return this.agentSwitchHandler.switch(sessionId, toAgent);
|
|
25194
26294
|
}
|
|
26295
|
+
/** Find a session by channel+thread, resuming from disk if not in memory. */
|
|
25195
26296
|
async getOrResumeSession(channelId, threadId) {
|
|
25196
26297
|
return this.sessionFactory.getOrResume(channelId, threadId);
|
|
25197
26298
|
}
|
|
26299
|
+
/** Find a session by ID, resuming from disk if not in memory. */
|
|
25198
26300
|
async getOrResumeSessionById(sessionId) {
|
|
25199
26301
|
return this.sessionFactory.getOrResumeById(sessionId);
|
|
25200
26302
|
}
|
|
26303
|
+
/**
|
|
26304
|
+
* Attach an additional adapter to an existing session (multi-adapter support).
|
|
26305
|
+
*
|
|
26306
|
+
* Creates a thread on the target adapter and connects a SessionBridge so the
|
|
26307
|
+
* session's agent events are forwarded to both the primary and attached adapters.
|
|
26308
|
+
*/
|
|
25201
26309
|
async attachAdapter(sessionId, adapterId) {
|
|
25202
26310
|
const session = this.sessionManager.getSession(sessionId);
|
|
25203
26311
|
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
@@ -25221,6 +26329,10 @@ ${text5}`;
|
|
|
25221
26329
|
});
|
|
25222
26330
|
return { threadId };
|
|
25223
26331
|
}
|
|
26332
|
+
/**
|
|
26333
|
+
* Detach a secondary adapter from a session. The primary adapter (channelId) cannot
|
|
26334
|
+
* be detached. Disconnects the bridge and cleans up thread mappings.
|
|
26335
|
+
*/
|
|
25224
26336
|
async detachAdapter(sessionId, adapterId) {
|
|
25225
26337
|
const session = this.sessionManager.getSession(sessionId);
|
|
25226
26338
|
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
@@ -25253,6 +26365,7 @@ ${text5}`;
|
|
|
25253
26365
|
platforms: this.buildPlatformsFromSession(session)
|
|
25254
26366
|
});
|
|
25255
26367
|
}
|
|
26368
|
+
/** Build the platforms map (adapter → thread/topic IDs) for persistence. */
|
|
25256
26369
|
buildPlatformsFromSession(session) {
|
|
25257
26370
|
const platforms = {};
|
|
25258
26371
|
for (const [adapterId, threadId] of session.threadIds) {
|
|
@@ -25284,8 +26397,13 @@ ${text5}`;
|
|
|
25284
26397
|
const bridge = this.createBridge(session, adapter, session.channelId);
|
|
25285
26398
|
bridge.connect();
|
|
25286
26399
|
}
|
|
25287
|
-
/**
|
|
25288
|
-
*
|
|
26400
|
+
/**
|
|
26401
|
+
* Create a SessionBridge for the given session and adapter.
|
|
26402
|
+
*
|
|
26403
|
+
* The bridge subscribes to Session events (agent output, status changes, naming)
|
|
26404
|
+
* and forwards them to the adapter for platform delivery. Disconnects any existing
|
|
26405
|
+
* bridge for the same adapter+session first to avoid duplicate event handlers.
|
|
26406
|
+
*/
|
|
25289
26407
|
createBridge(session, adapter, adapterId) {
|
|
25290
26408
|
const id = adapterId ?? adapter.name;
|
|
25291
26409
|
const key = this.bridgeKey(id, session.id);
|
|
@@ -25395,10 +26513,15 @@ var init_command_registry = __esm({
|
|
|
25395
26513
|
return this.getAll().filter((cmd) => cmd.category === category);
|
|
25396
26514
|
}
|
|
25397
26515
|
/**
|
|
25398
|
-
* Parse and execute a command string.
|
|
25399
|
-
*
|
|
25400
|
-
*
|
|
25401
|
-
*
|
|
26516
|
+
* Parse and execute a command string (e.g. "/greet hello world").
|
|
26517
|
+
*
|
|
26518
|
+
* Resolution order:
|
|
26519
|
+
* 1. Adapter-specific override (e.g. Telegram's version of /new)
|
|
26520
|
+
* 2. Short name or qualified name in the main registry
|
|
26521
|
+
*
|
|
26522
|
+
* Strips Telegram-style bot mentions (e.g. "/help@MyBot" → "help").
|
|
26523
|
+
* Returns `{ type: 'delegated' }` if the handler returns null/undefined
|
|
26524
|
+
* (meaning it handled the response itself, e.g. via assistant).
|
|
25402
26525
|
*/
|
|
25403
26526
|
async execute(commandString, baseArgs) {
|
|
25404
26527
|
const trimmed = commandString.trim();
|
|
@@ -26193,12 +27316,15 @@ var init_plugin_field_registry = __esm({
|
|
|
26193
27316
|
"use strict";
|
|
26194
27317
|
PluginFieldRegistry = class {
|
|
26195
27318
|
fields = /* @__PURE__ */ new Map();
|
|
27319
|
+
/** Register (or replace) the editable fields for a plugin. */
|
|
26196
27320
|
register(pluginName, fields) {
|
|
26197
27321
|
this.fields.set(pluginName, fields);
|
|
26198
27322
|
}
|
|
27323
|
+
/** Get the editable fields for a specific plugin. */
|
|
26199
27324
|
getForPlugin(pluginName) {
|
|
26200
27325
|
return this.fields.get(pluginName) ?? [];
|
|
26201
27326
|
}
|
|
27327
|
+
/** Get all fields grouped by plugin name. */
|
|
26202
27328
|
getAll() {
|
|
26203
27329
|
return new Map(this.fields);
|
|
26204
27330
|
}
|
|
@@ -27555,6 +28681,10 @@ var init_dev_loader = __esm({
|
|
|
27555
28681
|
constructor(pluginPath) {
|
|
27556
28682
|
this.pluginPath = path55.resolve(pluginPath);
|
|
27557
28683
|
}
|
|
28684
|
+
/**
|
|
28685
|
+
* Import the plugin's default export from dist/index.js.
|
|
28686
|
+
* Each call uses a unique URL query to bypass Node's ESM cache.
|
|
28687
|
+
*/
|
|
27558
28688
|
async load() {
|
|
27559
28689
|
const distIndex = path55.join(this.pluginPath, "dist", "index.js");
|
|
27560
28690
|
const srcIndex = path55.join(this.pluginPath, "src", "index.ts");
|
|
@@ -27572,9 +28702,11 @@ var init_dev_loader = __esm({
|
|
|
27572
28702
|
}
|
|
27573
28703
|
return plugin2;
|
|
27574
28704
|
}
|
|
28705
|
+
/** Returns the resolved absolute path to the plugin's root directory. */
|
|
27575
28706
|
getPluginPath() {
|
|
27576
28707
|
return this.pluginPath;
|
|
27577
28708
|
}
|
|
28709
|
+
/** Returns the path to the plugin's dist directory. */
|
|
27578
28710
|
getDistPath() {
|
|
27579
28711
|
return path55.join(this.pluginPath, "dist");
|
|
27580
28712
|
}
|