@openacp/cli 2026.410.1 → 2026.410.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{channel-CKXNnTy4.d.ts → channel-CFMUPzvH.d.ts} +239 -21
- package/dist/cli.d.ts +21 -0
- package/dist/cli.js +1143 -32
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1999 -35
- package/dist/index.js +1014 -31
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -265,6 +265,8 @@ var init_config_migrations = __esm({
|
|
|
265
265
|
log2 = createChildLogger({ module: "config-migrations" });
|
|
266
266
|
migrations = [
|
|
267
267
|
{
|
|
268
|
+
// v2025.x: instanceName was added to support multi-instance setups.
|
|
269
|
+
// Old configs lack this field — default to "Main" so the UI has a display name.
|
|
268
270
|
name: "add-instance-name",
|
|
269
271
|
apply(raw) {
|
|
270
272
|
if (raw.instanceName) return false;
|
|
@@ -274,6 +276,8 @@ var init_config_migrations = __esm({
|
|
|
274
276
|
}
|
|
275
277
|
},
|
|
276
278
|
{
|
|
279
|
+
// displayVerbosity was replaced by outputMode — remove the legacy key
|
|
280
|
+
// so it doesn't confuse Zod strict parsing or the config editor.
|
|
277
281
|
name: "delete-display-verbosity",
|
|
278
282
|
apply(raw) {
|
|
279
283
|
if (!("displayVerbosity" in raw)) return false;
|
|
@@ -283,6 +287,9 @@ var init_config_migrations = __esm({
|
|
|
283
287
|
}
|
|
284
288
|
},
|
|
285
289
|
{
|
|
290
|
+
// Instance IDs were originally only in instances.json (the global registry).
|
|
291
|
+
// This migration copies the ID into config.json so each instance is self-identifying
|
|
292
|
+
// without needing to cross-reference the registry.
|
|
286
293
|
name: "add-instance-id",
|
|
287
294
|
apply(raw, ctx) {
|
|
288
295
|
if (raw.id) return false;
|
|
@@ -457,10 +464,11 @@ var init_config = __esm({
|
|
|
457
464
|
sessionLogRetentionDays: z.number().default(30)
|
|
458
465
|
}).default({});
|
|
459
466
|
ConfigSchema = z.object({
|
|
467
|
+
/** Instance UUID, written once at creation time. */
|
|
460
468
|
id: z.string().optional(),
|
|
461
|
-
// instance UUID, written once at creation time
|
|
462
469
|
instanceName: z.string().optional(),
|
|
463
470
|
defaultAgent: z.string(),
|
|
471
|
+
// --- Workspace security & path resolution ---
|
|
464
472
|
workspace: z.object({
|
|
465
473
|
allowExternalWorkspaces: z.boolean().default(true),
|
|
466
474
|
security: z.object({
|
|
@@ -468,12 +476,16 @@ var init_config = __esm({
|
|
|
468
476
|
envWhitelist: z.array(z.string()).default([])
|
|
469
477
|
}).default({})
|
|
470
478
|
}).default({}),
|
|
479
|
+
// --- Logging ---
|
|
471
480
|
logging: LoggingSchema,
|
|
481
|
+
// --- Process lifecycle ---
|
|
472
482
|
runMode: z.enum(["foreground", "daemon"]).default("foreground"),
|
|
473
483
|
autoStart: z.boolean().default(false),
|
|
484
|
+
// --- Session persistence ---
|
|
474
485
|
sessionStore: z.object({
|
|
475
486
|
ttlDays: z.number().default(30)
|
|
476
487
|
}).default({}),
|
|
488
|
+
// --- Installed integration tracking (e.g. plugins installed via CLI) ---
|
|
477
489
|
integrations: z.record(
|
|
478
490
|
z.string(),
|
|
479
491
|
z.object({
|
|
@@ -481,7 +493,9 @@ var init_config = __esm({
|
|
|
481
493
|
installedAt: z.string().optional()
|
|
482
494
|
})
|
|
483
495
|
).default({}),
|
|
496
|
+
// --- Agent output verbosity control ---
|
|
484
497
|
outputMode: z.enum(["low", "medium", "high"]).default("medium").optional(),
|
|
498
|
+
// --- Multi-agent switching behavior ---
|
|
485
499
|
agentSwitch: z.object({
|
|
486
500
|
labelHistory: z.boolean().default(true)
|
|
487
501
|
}).default({})
|
|
@@ -497,6 +511,13 @@ var init_config = __esm({
|
|
|
497
511
|
super();
|
|
498
512
|
this.configPath = process.env.OPENACP_CONFIG_PATH || configPath || expandHome2("~/.openacp/config.json");
|
|
499
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Loads config from disk through the full validation pipeline:
|
|
516
|
+
* 1. Create default config if missing (first run)
|
|
517
|
+
* 2. Apply migrations for older config formats
|
|
518
|
+
* 3. Apply environment variable overrides
|
|
519
|
+
* 4. Validate against Zod schema — exits on failure
|
|
520
|
+
*/
|
|
500
521
|
async load() {
|
|
501
522
|
const dir = path3.dirname(this.configPath);
|
|
502
523
|
fs3.mkdirSync(dir, { recursive: true });
|
|
@@ -530,9 +551,17 @@ var init_config = __esm({
|
|
|
530
551
|
}
|
|
531
552
|
this.config = result.data;
|
|
532
553
|
}
|
|
554
|
+
/** Returns a deep clone of the current config to prevent external mutation. */
|
|
533
555
|
get() {
|
|
534
556
|
return structuredClone(this.config);
|
|
535
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Merges partial updates into the config file using atomic write (write tmp + rename).
|
|
560
|
+
*
|
|
561
|
+
* Validates the merged result before writing. If `changePath` is provided,
|
|
562
|
+
* emits a `config:changed` event with old and new values for that path,
|
|
563
|
+
* enabling hot-reload without restart.
|
|
564
|
+
*/
|
|
536
565
|
async save(updates, changePath) {
|
|
537
566
|
const oldConfig = this.config ? structuredClone(this.config) : void 0;
|
|
538
567
|
const raw = JSON.parse(fs3.readFileSync(this.configPath, "utf-8"));
|
|
@@ -554,9 +583,12 @@ var init_config = __esm({
|
|
|
554
583
|
}
|
|
555
584
|
}
|
|
556
585
|
/**
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
586
|
+
* Convenience wrapper for updating a single deeply-nested config field
|
|
587
|
+
* without constructing the full update object manually.
|
|
588
|
+
*
|
|
589
|
+
* Accepts a dot-path (e.g. "logging.level") and builds the nested
|
|
590
|
+
* update object internally before delegating to `save()`.
|
|
591
|
+
* Throws if the path contains prototype-pollution keys.
|
|
560
592
|
*/
|
|
561
593
|
async setPath(dotPath, value) {
|
|
562
594
|
const BLOCKED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
@@ -573,6 +605,13 @@ var init_config = __esm({
|
|
|
573
605
|
target[parts[parts.length - 1]] = value;
|
|
574
606
|
await this.save(updates, dotPath);
|
|
575
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Resolves a workspace path from user input.
|
|
610
|
+
*
|
|
611
|
+
* Supports three forms: no input (returns base dir), absolute/tilde paths
|
|
612
|
+
* (validated against allowExternalWorkspaces), and named workspaces
|
|
613
|
+
* (alphanumeric subdirectories under the base).
|
|
614
|
+
*/
|
|
576
615
|
resolveWorkspace(input2) {
|
|
577
616
|
const workspaceBase = path3.dirname(path3.dirname(this.configPath));
|
|
578
617
|
if (!input2) {
|
|
@@ -607,17 +646,26 @@ var init_config = __esm({
|
|
|
607
646
|
fs3.mkdirSync(namedPath, { recursive: true });
|
|
608
647
|
return namedPath;
|
|
609
648
|
}
|
|
649
|
+
/** 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. */
|
|
610
650
|
async exists() {
|
|
611
651
|
return fs3.existsSync(this.configPath);
|
|
612
652
|
}
|
|
653
|
+
/** Returns the resolved path to the config JSON file. */
|
|
613
654
|
getConfigPath() {
|
|
614
655
|
return this.configPath;
|
|
615
656
|
}
|
|
657
|
+
/** Writes a complete config object to disk, creating the directory if needed. Used during initial setup. */
|
|
616
658
|
async writeNew(config) {
|
|
617
659
|
const dir = path3.dirname(this.configPath);
|
|
618
660
|
fs3.mkdirSync(dir, { recursive: true });
|
|
619
661
|
fs3.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
|
|
620
662
|
}
|
|
663
|
+
/**
|
|
664
|
+
* Applies `OPENACP_*` environment variables as overrides to per-plugin settings.
|
|
665
|
+
*
|
|
666
|
+
* This lets users configure plugin values (bot tokens, ports, etc.) via env vars
|
|
667
|
+
* without editing settings files — useful for Docker, CI, and headless setups.
|
|
668
|
+
*/
|
|
621
669
|
async applyEnvToPluginSettings(settingsManager) {
|
|
622
670
|
const pluginOverrides = [
|
|
623
671
|
{ envVar: "OPENACP_TUNNEL_ENABLED", pluginName: "@openacp/tunnel", key: "enabled", transform: (v) => v === "true" },
|
|
@@ -644,6 +692,7 @@ var init_config = __esm({
|
|
|
644
692
|
}
|
|
645
693
|
}
|
|
646
694
|
}
|
|
695
|
+
/** Applies env var overrides to the raw config object before Zod validation. */
|
|
647
696
|
applyEnvOverrides(raw) {
|
|
648
697
|
const overrides = [
|
|
649
698
|
["OPENACP_DEFAULT_AGENT", ["defaultAgent"]],
|
|
@@ -674,6 +723,7 @@ var init_config = __esm({
|
|
|
674
723
|
raw.logging.level = "debug";
|
|
675
724
|
}
|
|
676
725
|
}
|
|
726
|
+
/** Recursively merges source into target, skipping prototype-pollution keys. */
|
|
677
727
|
deepMerge(target, source) {
|
|
678
728
|
const DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
679
729
|
for (const key of Object.keys(source)) {
|
|
@@ -769,44 +819,75 @@ var init_events = __esm({
|
|
|
769
819
|
};
|
|
770
820
|
BusEvent = {
|
|
771
821
|
// --- Session lifecycle ---
|
|
822
|
+
/** Fired when a new session is created and ready. */
|
|
772
823
|
SESSION_CREATED: "session:created",
|
|
824
|
+
/** Fired when session metadata changes (status, name, overrides). */
|
|
773
825
|
SESSION_UPDATED: "session:updated",
|
|
826
|
+
/** Fired when a session record is deleted from the store. */
|
|
774
827
|
SESSION_DELETED: "session:deleted",
|
|
828
|
+
/** Fired when a session ends (agent finished or error). */
|
|
775
829
|
SESSION_ENDED: "session:ended",
|
|
830
|
+
/** Fired when a session receives its auto-generated name. */
|
|
776
831
|
SESSION_NAMED: "session:named",
|
|
832
|
+
/** Fired after a new session thread is created and bridge connected. */
|
|
777
833
|
SESSION_THREAD_READY: "session:threadReady",
|
|
834
|
+
/** Fired when an agent's config options change (adapters update control UIs). */
|
|
778
835
|
SESSION_CONFIG_CHANGED: "session:configChanged",
|
|
836
|
+
/** Fired during agent switch lifecycle (starting/succeeded/failed). */
|
|
779
837
|
SESSION_AGENT_SWITCH: "session:agentSwitch",
|
|
780
838
|
// --- Agent ---
|
|
839
|
+
/** Fired for every agent event (text, tool_call, usage, etc.). */
|
|
781
840
|
AGENT_EVENT: "agent:event",
|
|
841
|
+
/** Fired when a prompt is sent to the agent. */
|
|
782
842
|
AGENT_PROMPT: "agent:prompt",
|
|
783
843
|
// --- Permissions ---
|
|
844
|
+
/** Fired when the agent requests user permission (blocks until resolved). */
|
|
784
845
|
PERMISSION_REQUEST: "permission:request",
|
|
846
|
+
/** Fired after a permission request is resolved (approved or denied). */
|
|
785
847
|
PERMISSION_RESOLVED: "permission:resolved",
|
|
786
848
|
// --- Message visibility ---
|
|
849
|
+
/** Fired when a user message is queued (for cross-adapter input visibility). */
|
|
787
850
|
MESSAGE_QUEUED: "message:queued",
|
|
851
|
+
/** Fired when a queued message starts processing. */
|
|
788
852
|
MESSAGE_PROCESSING: "message:processing",
|
|
789
853
|
// --- System lifecycle ---
|
|
854
|
+
/** Fired after kernel (core + plugin infrastructure) has booted. */
|
|
790
855
|
KERNEL_BOOTED: "kernel:booted",
|
|
856
|
+
/** Fired when the system is fully ready (all adapters connected). */
|
|
791
857
|
SYSTEM_READY: "system:ready",
|
|
858
|
+
/** Fired during graceful shutdown. */
|
|
792
859
|
SYSTEM_SHUTDOWN: "system:shutdown",
|
|
860
|
+
/** Fired when all system commands are registered and available. */
|
|
793
861
|
SYSTEM_COMMANDS_READY: "system:commands-ready",
|
|
794
862
|
// --- Plugin lifecycle ---
|
|
863
|
+
/** Fired when a plugin loads successfully. */
|
|
795
864
|
PLUGIN_LOADED: "plugin:loaded",
|
|
865
|
+
/** Fired when a plugin fails to load. */
|
|
796
866
|
PLUGIN_FAILED: "plugin:failed",
|
|
867
|
+
/** Fired when a plugin is disabled (e.g., missing config). */
|
|
797
868
|
PLUGIN_DISABLED: "plugin:disabled",
|
|
869
|
+
/** Fired when a plugin is unloaded during shutdown. */
|
|
798
870
|
PLUGIN_UNLOADED: "plugin:unloaded",
|
|
799
871
|
// --- Usage ---
|
|
872
|
+
/** Fired when a token usage record is captured (consumed by usage plugin). */
|
|
800
873
|
USAGE_RECORDED: "usage:recorded"
|
|
801
874
|
};
|
|
802
875
|
SessionEv = {
|
|
876
|
+
/** Agent produced an event (text, tool_call, etc.) during a turn. */
|
|
803
877
|
AGENT_EVENT: "agent_event",
|
|
878
|
+
/** Agent is requesting user permission — blocks until resolved. */
|
|
804
879
|
PERMISSION_REQUEST: "permission_request",
|
|
880
|
+
/** Session ended (agent finished, cancelled, or errored). */
|
|
805
881
|
SESSION_END: "session_end",
|
|
882
|
+
/** Session status changed (e.g., initializing → active). */
|
|
806
883
|
STATUS_CHANGE: "status_change",
|
|
884
|
+
/** Session received an auto-generated name from the first response. */
|
|
807
885
|
NAMED: "named",
|
|
886
|
+
/** An unrecoverable error occurred in the session. */
|
|
808
887
|
ERROR: "error",
|
|
888
|
+
/** The session's prompt count changed (used for UI counters). */
|
|
809
889
|
PROMPT_COUNT_CHANGED: "prompt_count_changed",
|
|
890
|
+
/** A new prompt turn started (provides TurnContext for middleware). */
|
|
810
891
|
TURN_STARTED: "turn_started"
|
|
811
892
|
};
|
|
812
893
|
}
|
|
@@ -1563,13 +1644,16 @@ var init_settings_manager = __esm({
|
|
|
1563
1644
|
constructor(basePath) {
|
|
1564
1645
|
this.basePath = basePath;
|
|
1565
1646
|
}
|
|
1647
|
+
/** Returns the base path for all plugin settings directories. */
|
|
1566
1648
|
getBasePath() {
|
|
1567
1649
|
return this.basePath;
|
|
1568
1650
|
}
|
|
1651
|
+
/** Create a SettingsAPI instance scoped to a specific plugin. */
|
|
1569
1652
|
createAPI(pluginName) {
|
|
1570
1653
|
const settingsPath = this.getSettingsPath(pluginName);
|
|
1571
1654
|
return new SettingsAPIImpl(settingsPath);
|
|
1572
1655
|
}
|
|
1656
|
+
/** Load a plugin's settings from disk. Returns empty object if file doesn't exist. */
|
|
1573
1657
|
async loadSettings(pluginName) {
|
|
1574
1658
|
const settingsPath = this.getSettingsPath(pluginName);
|
|
1575
1659
|
try {
|
|
@@ -1579,6 +1663,7 @@ var init_settings_manager = __esm({
|
|
|
1579
1663
|
return {};
|
|
1580
1664
|
}
|
|
1581
1665
|
}
|
|
1666
|
+
/** Validate settings against a Zod schema. Returns valid if no schema is provided. */
|
|
1582
1667
|
validateSettings(_pluginName, settings, schema) {
|
|
1583
1668
|
if (!schema) return { valid: true };
|
|
1584
1669
|
const result = schema.safeParse(settings);
|
|
@@ -1590,12 +1675,14 @@ var init_settings_manager = __esm({
|
|
|
1590
1675
|
)
|
|
1591
1676
|
};
|
|
1592
1677
|
}
|
|
1678
|
+
/** Resolve the absolute path to a plugin's settings.json file. */
|
|
1593
1679
|
getSettingsPath(pluginName) {
|
|
1594
1680
|
return path16.join(this.basePath, pluginName, "settings.json");
|
|
1595
1681
|
}
|
|
1596
1682
|
async getPluginSettings(pluginName) {
|
|
1597
1683
|
return this.loadSettings(pluginName);
|
|
1598
1684
|
}
|
|
1685
|
+
/** Merge updates into existing settings (shallow merge). */
|
|
1599
1686
|
async updatePluginSettings(pluginName, updates) {
|
|
1600
1687
|
const api = this.createAPI(pluginName);
|
|
1601
1688
|
const current = await api.getAll();
|
|
@@ -2321,6 +2408,12 @@ var init_doctor = __esm({
|
|
|
2321
2408
|
this.dryRun = options?.dryRun ?? false;
|
|
2322
2409
|
this.dataDir = options.dataDir;
|
|
2323
2410
|
}
|
|
2411
|
+
/**
|
|
2412
|
+
* Executes all checks and returns an aggregated report.
|
|
2413
|
+
*
|
|
2414
|
+
* Safe fixes are applied inline (mutating CheckResult.message to show "Fixed").
|
|
2415
|
+
* Risky fixes are deferred to `report.pendingFixes` for user confirmation.
|
|
2416
|
+
*/
|
|
2324
2417
|
async runAll() {
|
|
2325
2418
|
const ctx = await this.buildContext();
|
|
2326
2419
|
const checks = [...ALL_CHECKS].sort((a, b) => a.order - b.order);
|
|
@@ -2368,6 +2461,7 @@ var init_doctor = __esm({
|
|
|
2368
2461
|
}
|
|
2369
2462
|
return { categories, summary, pendingFixes };
|
|
2370
2463
|
}
|
|
2464
|
+
/** Constructs the shared context used by all checks — loads config if available. */
|
|
2371
2465
|
async buildContext() {
|
|
2372
2466
|
const dataDir = this.dataDir;
|
|
2373
2467
|
const configPath = process.env.OPENACP_CONFIG_PATH || path22.join(dataDir, "config.json");
|
|
@@ -2432,6 +2526,7 @@ var init_instance_registry = __esm({
|
|
|
2432
2526
|
this.registryPath = registryPath;
|
|
2433
2527
|
}
|
|
2434
2528
|
data = { version: 1, instances: {} };
|
|
2529
|
+
/** Load the registry from disk. If the file is missing or corrupt, starts fresh. */
|
|
2435
2530
|
load() {
|
|
2436
2531
|
try {
|
|
2437
2532
|
const raw = fs27.readFileSync(this.registryPath, "utf-8");
|
|
@@ -2459,26 +2554,33 @@ var init_instance_registry = __esm({
|
|
|
2459
2554
|
this.save();
|
|
2460
2555
|
}
|
|
2461
2556
|
}
|
|
2557
|
+
/** Persist the registry to disk, creating parent directories if needed. */
|
|
2462
2558
|
save() {
|
|
2463
2559
|
const dir = path25.dirname(this.registryPath);
|
|
2464
2560
|
fs27.mkdirSync(dir, { recursive: true });
|
|
2465
2561
|
fs27.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
|
|
2466
2562
|
}
|
|
2563
|
+
/** Add or update an instance entry in the registry. Does not persist — call save() after. */
|
|
2467
2564
|
register(id, root) {
|
|
2468
2565
|
this.data.instances[id] = { id, root };
|
|
2469
2566
|
}
|
|
2567
|
+
/** Remove an instance entry. Does not persist — call save() after. */
|
|
2470
2568
|
remove(id) {
|
|
2471
2569
|
delete this.data.instances[id];
|
|
2472
2570
|
}
|
|
2571
|
+
/** Look up an instance by its ID. */
|
|
2473
2572
|
get(id) {
|
|
2474
2573
|
return this.data.instances[id];
|
|
2475
2574
|
}
|
|
2575
|
+
/** Look up an instance by its root directory path. */
|
|
2476
2576
|
getByRoot(root) {
|
|
2477
2577
|
return Object.values(this.data.instances).find((e) => e.root === root);
|
|
2478
2578
|
}
|
|
2579
|
+
/** Returns all registered instances. */
|
|
2479
2580
|
list() {
|
|
2480
2581
|
return Object.values(this.data.instances);
|
|
2481
2582
|
}
|
|
2583
|
+
/** Returns `baseId` if available, otherwise appends `-2`, `-3`, etc. until unique. */
|
|
2482
2584
|
uniqueId(baseId) {
|
|
2483
2585
|
if (!this.data.instances[baseId]) return baseId;
|
|
2484
2586
|
let n = 2;
|
|
@@ -2735,6 +2837,12 @@ var init_notification = __esm({
|
|
|
2735
2837
|
constructor(adapters) {
|
|
2736
2838
|
this.adapters = adapters;
|
|
2737
2839
|
}
|
|
2840
|
+
/**
|
|
2841
|
+
* Send a notification to a specific channel adapter.
|
|
2842
|
+
*
|
|
2843
|
+
* Failures are swallowed — notifications are best-effort and must not crash
|
|
2844
|
+
* the session or caller (e.g. on session completion).
|
|
2845
|
+
*/
|
|
2738
2846
|
async notify(channelId, notification) {
|
|
2739
2847
|
const adapter = this.adapters.get(channelId);
|
|
2740
2848
|
if (!adapter) return;
|
|
@@ -2743,6 +2851,12 @@ var init_notification = __esm({
|
|
|
2743
2851
|
} catch {
|
|
2744
2852
|
}
|
|
2745
2853
|
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Broadcast a notification to every registered adapter.
|
|
2856
|
+
*
|
|
2857
|
+
* Used for system-wide alerts (e.g. global budget exhausted). Each adapter
|
|
2858
|
+
* failure is isolated so one broken adapter cannot block the rest.
|
|
2859
|
+
*/
|
|
2746
2860
|
async notifyAll(notification) {
|
|
2747
2861
|
for (const adapter of this.adapters.values()) {
|
|
2748
2862
|
try {
|
|
@@ -2830,6 +2944,14 @@ var init_file_service = __esm({
|
|
|
2830
2944
|
}
|
|
2831
2945
|
return removed;
|
|
2832
2946
|
}
|
|
2947
|
+
/**
|
|
2948
|
+
* Persist a file to the session's directory and return an `Attachment` descriptor.
|
|
2949
|
+
*
|
|
2950
|
+
* The file name is sanitized (non-safe characters replaced with `_`) and prefixed
|
|
2951
|
+
* with a millisecond timestamp to prevent name collisions across multiple saves in
|
|
2952
|
+
* the same session. The original `fileName` is preserved in the returned descriptor
|
|
2953
|
+
* so the user-facing name is not lost.
|
|
2954
|
+
*/
|
|
2833
2955
|
async saveFile(sessionId, fileName, data, mimeType) {
|
|
2834
2956
|
const sessionDir = path32.join(this.baseDir, sessionId);
|
|
2835
2957
|
await fs32.promises.mkdir(sessionDir, { recursive: true });
|
|
@@ -2844,6 +2966,10 @@ var init_file_service = __esm({
|
|
|
2844
2966
|
size: data.length
|
|
2845
2967
|
};
|
|
2846
2968
|
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Build an `Attachment` descriptor for a file that already exists on disk.
|
|
2971
|
+
* Returns `null` if the path does not exist or is not a regular file.
|
|
2972
|
+
*/
|
|
2847
2973
|
async resolveFile(filePath) {
|
|
2848
2974
|
try {
|
|
2849
2975
|
const stat = await fs32.promises.stat(filePath);
|
|
@@ -2888,6 +3014,7 @@ var init_file_service = __esm({
|
|
|
2888
3014
|
extensionFromMime(mimeType) {
|
|
2889
3015
|
return _FileService.extensionFromMime(mimeType);
|
|
2890
3016
|
}
|
|
3017
|
+
/** Returns the canonical file extension for a given MIME type (e.g. `"image/png"` → `".png"`). */
|
|
2891
3018
|
static extensionFromMime(mimeType) {
|
|
2892
3019
|
return MIME_TO_EXT[mimeType] || ".bin";
|
|
2893
3020
|
}
|
|
@@ -2905,6 +3032,16 @@ var init_security_guard = __esm({
|
|
|
2905
3032
|
this.getSecurityConfig = getSecurityConfig;
|
|
2906
3033
|
this.sessionManager = sessionManager;
|
|
2907
3034
|
}
|
|
3035
|
+
/**
|
|
3036
|
+
* Returns `{ allowed: true }` when the message may proceed, or
|
|
3037
|
+
* `{ allowed: false, reason }` when it should be blocked.
|
|
3038
|
+
*
|
|
3039
|
+
* Two checks run in order:
|
|
3040
|
+
* 1. **Allowlist** — if `allowedUserIds` is non-empty, the user's ID (coerced to string)
|
|
3041
|
+
* must appear in the list. Telegram/Slack IDs are numbers, so coercion is required.
|
|
3042
|
+
* 2. **Session cap** — counts sessions in `active` or `initializing` state. `initializing`
|
|
3043
|
+
* is included because a session holds resources before it reaches `active`.
|
|
3044
|
+
*/
|
|
2908
3045
|
async checkAccess(message) {
|
|
2909
3046
|
const config = await this.getSecurityConfig();
|
|
2910
3047
|
const allowedIds = config.allowedUserIds ?? [];
|
|
@@ -3298,6 +3435,13 @@ var init_sse_manager = __esm({
|
|
|
3298
3435
|
sseCleanupHandlers = /* @__PURE__ */ new Map();
|
|
3299
3436
|
healthInterval;
|
|
3300
3437
|
boundHandlers = [];
|
|
3438
|
+
/**
|
|
3439
|
+
* Subscribes to EventBus events and starts the health heartbeat interval.
|
|
3440
|
+
*
|
|
3441
|
+
* Must be called after the HTTP server is listening, not during plugin setup —
|
|
3442
|
+
* before `setup()` runs, no EventBus listeners are registered, so any events
|
|
3443
|
+
* emitted in the interim are silently missed.
|
|
3444
|
+
*/
|
|
3301
3445
|
setup() {
|
|
3302
3446
|
if (!this.eventBus) return;
|
|
3303
3447
|
const events = [
|
|
@@ -3331,6 +3475,13 @@ var init_sse_manager = __esm({
|
|
|
3331
3475
|
});
|
|
3332
3476
|
}, 15e3);
|
|
3333
3477
|
}
|
|
3478
|
+
/**
|
|
3479
|
+
* Handles an incoming SSE request by upgrading the HTTP response to an event stream.
|
|
3480
|
+
*
|
|
3481
|
+
* The response is kept open indefinitely; cleanup runs when the client disconnects.
|
|
3482
|
+
* An initial `: connected` comment is written immediately so proxies and browsers
|
|
3483
|
+
* flush the response headers before the first real event arrives.
|
|
3484
|
+
*/
|
|
3334
3485
|
handleRequest(req, res) {
|
|
3335
3486
|
if (this.sseConnections.size >= MAX_SSE_CONNECTIONS) {
|
|
3336
3487
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
@@ -3365,6 +3516,13 @@ var init_sse_manager = __esm({
|
|
|
3365
3516
|
this.sseCleanupHandlers.set(res, cleanup);
|
|
3366
3517
|
req.on("close", cleanup);
|
|
3367
3518
|
}
|
|
3519
|
+
/**
|
|
3520
|
+
* Broadcasts an event to all connected SSE clients.
|
|
3521
|
+
*
|
|
3522
|
+
* Session-scoped events (agent_event, permission_request, etc.) are filtered per-connection:
|
|
3523
|
+
* a client that subscribed with `?sessionId=X` only receives events for session X.
|
|
3524
|
+
* Global events (session_created, health) are delivered to every client.
|
|
3525
|
+
*/
|
|
3368
3526
|
broadcast(event, data) {
|
|
3369
3527
|
const payload = `event: ${event}
|
|
3370
3528
|
data: ${JSON.stringify(data)}
|
|
@@ -3400,6 +3558,12 @@ data: ${JSON.stringify(data)}
|
|
|
3400
3558
|
this.handleRequest(request.raw, reply.raw);
|
|
3401
3559
|
};
|
|
3402
3560
|
}
|
|
3561
|
+
/**
|
|
3562
|
+
* Stops the heartbeat, removes all EventBus listeners, and closes all open SSE connections.
|
|
3563
|
+
*
|
|
3564
|
+
* Only listeners registered by this instance are removed — other consumers on the same
|
|
3565
|
+
* EventBus are unaffected.
|
|
3566
|
+
*/
|
|
3403
3567
|
stop() {
|
|
3404
3568
|
if (this.healthInterval) clearInterval(this.healthInterval);
|
|
3405
3569
|
if (this.eventBus) {
|
|
@@ -3459,9 +3623,15 @@ var init_static_server = __esm({
|
|
|
3459
3623
|
}
|
|
3460
3624
|
}
|
|
3461
3625
|
}
|
|
3626
|
+
/** Returns true if a UI build was found and static serving is active. */
|
|
3462
3627
|
isAvailable() {
|
|
3463
3628
|
return this.uiDir !== void 0;
|
|
3464
3629
|
}
|
|
3630
|
+
/**
|
|
3631
|
+
* Attempts to serve a static file or SPA fallback for the given request.
|
|
3632
|
+
*
|
|
3633
|
+
* @returns true if the response was handled, false if the caller should return a 404.
|
|
3634
|
+
*/
|
|
3465
3635
|
serve(req, res) {
|
|
3466
3636
|
if (!this.uiDir) return false;
|
|
3467
3637
|
const urlPath = (req.url || "/").split("?")[0];
|
|
@@ -3523,23 +3693,38 @@ var init_speech_service = __esm({
|
|
|
3523
3693
|
setProviderFactory(factory) {
|
|
3524
3694
|
this.providerFactory = factory;
|
|
3525
3695
|
}
|
|
3696
|
+
/** Register an STT provider by name. Overwrites any existing provider with the same name. */
|
|
3526
3697
|
registerSTTProvider(name, provider) {
|
|
3527
3698
|
this.sttProviders.set(name, provider);
|
|
3528
3699
|
}
|
|
3700
|
+
/** Register a TTS provider by name. Called by external TTS plugins (e.g. msedge-tts-plugin). */
|
|
3529
3701
|
registerTTSProvider(name, provider) {
|
|
3530
3702
|
this.ttsProviders.set(name, provider);
|
|
3531
3703
|
}
|
|
3704
|
+
/** Remove a TTS provider — called by external plugins on teardown. */
|
|
3532
3705
|
unregisterTTSProvider(name) {
|
|
3533
3706
|
this.ttsProviders.delete(name);
|
|
3534
3707
|
}
|
|
3708
|
+
/** Returns true if an STT provider is configured and has credentials. */
|
|
3535
3709
|
isSTTAvailable() {
|
|
3536
3710
|
const { provider, providers } = this.config.stt;
|
|
3537
3711
|
return provider !== null && providers[provider]?.apiKey !== void 0;
|
|
3538
3712
|
}
|
|
3713
|
+
/**
|
|
3714
|
+
* Returns true if a TTS provider is configured and an implementation is registered.
|
|
3715
|
+
*
|
|
3716
|
+
* Config alone is not enough — the TTS provider plugin must have registered
|
|
3717
|
+
* its implementation via `registerTTSProvider` before this returns true.
|
|
3718
|
+
*/
|
|
3539
3719
|
isTTSAvailable() {
|
|
3540
3720
|
const provider = this.config.tts.provider;
|
|
3541
3721
|
return provider !== null && this.ttsProviders.has(provider);
|
|
3542
3722
|
}
|
|
3723
|
+
/**
|
|
3724
|
+
* Transcribes audio using the configured STT provider.
|
|
3725
|
+
*
|
|
3726
|
+
* @throws if no STT provider is configured or if the named provider is not registered.
|
|
3727
|
+
*/
|
|
3543
3728
|
async transcribe(audioBuffer, mimeType, options) {
|
|
3544
3729
|
const providerName = this.config.stt.provider;
|
|
3545
3730
|
if (!providerName || !this.config.stt.providers[providerName]?.apiKey) {
|
|
@@ -3551,6 +3736,11 @@ var init_speech_service = __esm({
|
|
|
3551
3736
|
}
|
|
3552
3737
|
return provider.transcribe(audioBuffer, mimeType, options);
|
|
3553
3738
|
}
|
|
3739
|
+
/**
|
|
3740
|
+
* Synthesizes speech using the configured TTS provider.
|
|
3741
|
+
*
|
|
3742
|
+
* @throws if no TTS provider is configured or if the named provider is not registered.
|
|
3743
|
+
*/
|
|
3554
3744
|
async synthesize(text3, options) {
|
|
3555
3745
|
const providerName = this.config.tts.provider;
|
|
3556
3746
|
if (!providerName) {
|
|
@@ -3562,10 +3752,17 @@ var init_speech_service = __esm({
|
|
|
3562
3752
|
}
|
|
3563
3753
|
return provider.synthesize(text3, options);
|
|
3564
3754
|
}
|
|
3755
|
+
/** Replace the active config without rebuilding providers. Use `refreshProviders` to also rebuild. */
|
|
3565
3756
|
updateConfig(config) {
|
|
3566
3757
|
this.config = config;
|
|
3567
3758
|
}
|
|
3568
|
-
/**
|
|
3759
|
+
/**
|
|
3760
|
+
* Reloads TTS and STT providers from a new config snapshot.
|
|
3761
|
+
*
|
|
3762
|
+
* Called after config changes or plugin hot-reload. Factory-managed providers are
|
|
3763
|
+
* rebuilt via the registered `ProviderFactory`; externally-registered providers
|
|
3764
|
+
* (e.g. from `@openacp/msedge-tts-plugin`) are preserved rather than discarded.
|
|
3765
|
+
*/
|
|
3569
3766
|
refreshProviders(newConfig) {
|
|
3570
3767
|
this.config = newConfig;
|
|
3571
3768
|
if (this.providerFactory) {
|
|
@@ -3605,6 +3802,12 @@ var init_groq = __esm({
|
|
|
3605
3802
|
this.defaultModel = defaultModel;
|
|
3606
3803
|
}
|
|
3607
3804
|
name = "groq";
|
|
3805
|
+
/**
|
|
3806
|
+
* Transcribes audio using the Groq Whisper API.
|
|
3807
|
+
*
|
|
3808
|
+
* `verbose_json` response format is requested so the API returns language
|
|
3809
|
+
* detection and duration metadata alongside the transcript text.
|
|
3810
|
+
*/
|
|
3608
3811
|
async transcribe(audioBuffer, mimeType, options) {
|
|
3609
3812
|
const ext = mimeToExt(mimeType);
|
|
3610
3813
|
const form = new FormData();
|
|
@@ -3707,6 +3910,12 @@ var init_context_manager = __esm({
|
|
|
3707
3910
|
constructor(cachePath) {
|
|
3708
3911
|
this.cache = new ContextCache(cachePath);
|
|
3709
3912
|
}
|
|
3913
|
+
/**
|
|
3914
|
+
* Wire in the history store after construction.
|
|
3915
|
+
*
|
|
3916
|
+
* Injected separately because the history store (backed by the context plugin's
|
|
3917
|
+
* recorder) may not be ready when ContextManager is first instantiated.
|
|
3918
|
+
*/
|
|
3710
3919
|
setHistoryStore(store) {
|
|
3711
3920
|
this.historyStore = store;
|
|
3712
3921
|
}
|
|
@@ -3722,19 +3931,43 @@ var init_context_manager = __esm({
|
|
|
3722
3931
|
async flushSession(sessionId) {
|
|
3723
3932
|
if (this.sessionFlusher) await this.sessionFlusher(sessionId);
|
|
3724
3933
|
}
|
|
3934
|
+
/**
|
|
3935
|
+
* Read the raw history for a session directly from the history store.
|
|
3936
|
+
*
|
|
3937
|
+
* Returns null if no historyStore has been configured via `setHistoryStore()`,
|
|
3938
|
+
* or if the session has no recorded history.
|
|
3939
|
+
*/
|
|
3725
3940
|
async getHistory(sessionId) {
|
|
3726
3941
|
if (!this.historyStore) return null;
|
|
3727
3942
|
return this.historyStore.read(sessionId);
|
|
3728
3943
|
}
|
|
3944
|
+
/**
|
|
3945
|
+
* Register a provider. Providers are queried in insertion order.
|
|
3946
|
+
* Register higher-priority sources (e.g. local history) before lower-priority ones (e.g. entire).
|
|
3947
|
+
*/
|
|
3729
3948
|
register(provider) {
|
|
3730
3949
|
this.providers.push(provider);
|
|
3731
3950
|
}
|
|
3951
|
+
/**
|
|
3952
|
+
* Return the first provider that reports itself available for the given repo.
|
|
3953
|
+
*
|
|
3954
|
+
* This is a availability check — it returns the highest-priority available provider
|
|
3955
|
+
* (i.e. the first registered one that passes `isAvailable`), not necessarily the
|
|
3956
|
+
* one that would yield the richest context for a specific query.
|
|
3957
|
+
*/
|
|
3732
3958
|
async getProvider(repoPath) {
|
|
3733
3959
|
for (const provider of this.providers) {
|
|
3734
3960
|
if (await provider.isAvailable(repoPath)) return provider;
|
|
3735
3961
|
}
|
|
3736
3962
|
return null;
|
|
3737
3963
|
}
|
|
3964
|
+
/**
|
|
3965
|
+
* List sessions using the same provider-waterfall logic as `buildContext`.
|
|
3966
|
+
*
|
|
3967
|
+
* Tries each registered provider in order, returning the first non-empty result.
|
|
3968
|
+
* Unlike `buildContext`, results are not cached — callers should avoid calling
|
|
3969
|
+
* this in hot paths.
|
|
3970
|
+
*/
|
|
3738
3971
|
async listSessions(query) {
|
|
3739
3972
|
for (const provider of this.providers) {
|
|
3740
3973
|
if (!await provider.isAvailable(query.repoPath)) continue;
|
|
@@ -3743,6 +3976,13 @@ var init_context_manager = __esm({
|
|
|
3743
3976
|
}
|
|
3744
3977
|
return null;
|
|
3745
3978
|
}
|
|
3979
|
+
/**
|
|
3980
|
+
* Build a context block for injection into an agent prompt.
|
|
3981
|
+
*
|
|
3982
|
+
* Tries each registered provider in order. Results are cached by (repoPath + queryKey)
|
|
3983
|
+
* to avoid redundant disk reads. Pass `options.noCache = true` when the caller knows
|
|
3984
|
+
* the history just changed (e.g. immediately after an agent switch + flush).
|
|
3985
|
+
*/
|
|
3746
3986
|
async buildContext(query, options) {
|
|
3747
3987
|
const queryKey = `${query.type}:${query.value}:${options?.limit ?? ""}:${options?.maxTokens ?? ""}:${options?.labelAgent ?? ""}`;
|
|
3748
3988
|
if (!options?.noCache) {
|
|
@@ -3905,8 +4145,9 @@ var init_checkpoint_reader = __esm({
|
|
|
3905
4145
|
sessionIndex: String(idx),
|
|
3906
4146
|
transcriptPath,
|
|
3907
4147
|
createdAt,
|
|
4148
|
+
// endedAt isn't stored in the checkpoint metadata; EntireProvider fills
|
|
4149
|
+
// it from the last turn timestamp when parsing the JSONL transcript.
|
|
3908
4150
|
endedAt: createdAt,
|
|
3909
|
-
// will be filled from JSONL by conversation builder
|
|
3910
4151
|
branch: smeta.branch ?? cpMeta.branch ?? "",
|
|
3911
4152
|
agent: smeta.agent ?? "",
|
|
3912
4153
|
turnCount: smeta.session_metrics?.turn_count ?? 0,
|
|
@@ -4735,11 +4976,21 @@ var init_messaging_adapter = __esm({
|
|
|
4735
4976
|
this.adapterConfig = adapterConfig;
|
|
4736
4977
|
}
|
|
4737
4978
|
// === Message dispatch flow ===
|
|
4979
|
+
/**
|
|
4980
|
+
* Entry point for all outbound messages from sessions to the platform.
|
|
4981
|
+
* Resolves the current verbosity, filters messages that should be hidden,
|
|
4982
|
+
* then dispatches to the appropriate type-specific handler.
|
|
4983
|
+
*/
|
|
4738
4984
|
async sendMessage(sessionId, content) {
|
|
4739
4985
|
const verbosity = this.getVerbosity();
|
|
4740
4986
|
if (!this.shouldDisplay(content, verbosity)) return;
|
|
4741
4987
|
await this.dispatchMessage(sessionId, content, verbosity);
|
|
4742
4988
|
}
|
|
4989
|
+
/**
|
|
4990
|
+
* Routes a message to its type-specific handler.
|
|
4991
|
+
* Subclasses can override this for custom dispatch logic, but typically
|
|
4992
|
+
* override individual handle* methods instead.
|
|
4993
|
+
*/
|
|
4743
4994
|
async dispatchMessage(sessionId, content, verbosity) {
|
|
4744
4995
|
switch (content.type) {
|
|
4745
4996
|
case "text":
|
|
@@ -4777,6 +5028,8 @@ var init_messaging_adapter = __esm({
|
|
|
4777
5028
|
}
|
|
4778
5029
|
}
|
|
4779
5030
|
// === Default handlers — all protected, all overridable ===
|
|
5031
|
+
// Each handler is a no-op by default. Subclasses override only the message
|
|
5032
|
+
// types they support (e.g., Telegram overrides handleText, handleToolCall, etc.).
|
|
4780
5033
|
async handleText(_sessionId, _content) {
|
|
4781
5034
|
}
|
|
4782
5035
|
async handleThought(_sessionId, _content, _verbosity) {
|
|
@@ -4810,6 +5063,10 @@ var init_messaging_adapter = __esm({
|
|
|
4810
5063
|
async handleResourceLink(_sessionId, _content) {
|
|
4811
5064
|
}
|
|
4812
5065
|
// === Helpers ===
|
|
5066
|
+
/**
|
|
5067
|
+
* Resolves the current output verbosity by checking (in priority order):
|
|
5068
|
+
* per-channel config, global config, then adapter default. Falls back to "medium".
|
|
5069
|
+
*/
|
|
4813
5070
|
getVerbosity() {
|
|
4814
5071
|
const config = this.context.configManager.get();
|
|
4815
5072
|
const channelConfig = config.channels;
|
|
@@ -4818,6 +5075,13 @@ var init_messaging_adapter = __esm({
|
|
|
4818
5075
|
if (v === "low" || v === "high") return v;
|
|
4819
5076
|
return "medium";
|
|
4820
5077
|
}
|
|
5078
|
+
/**
|
|
5079
|
+
* Determines whether a message should be displayed at the given verbosity.
|
|
5080
|
+
*
|
|
5081
|
+
* Noise filtering: tool calls matching noise rules (e.g., `ls`, `glob`, `grep`)
|
|
5082
|
+
* are hidden at medium/low verbosity to reduce clutter. Thoughts and usage
|
|
5083
|
+
* stats are hidden entirely at "low".
|
|
5084
|
+
*/
|
|
4821
5085
|
shouldDisplay(content, verbosity) {
|
|
4822
5086
|
if (verbosity === "low" && HIDDEN_ON_LOW.has(content.type)) return false;
|
|
4823
5087
|
if (content.type === "tool_call") {
|
|
@@ -5044,6 +5308,13 @@ var init_send_queue = __esm({
|
|
|
5044
5308
|
get pending() {
|
|
5045
5309
|
return this.items.length;
|
|
5046
5310
|
}
|
|
5311
|
+
/**
|
|
5312
|
+
* Queues an async operation for rate-limited execution.
|
|
5313
|
+
*
|
|
5314
|
+
* For text-type items with a key, replaces any existing queued item with
|
|
5315
|
+
* the same key (deduplication). This is used for streaming draft updates
|
|
5316
|
+
* where only the latest content matters.
|
|
5317
|
+
*/
|
|
5047
5318
|
enqueue(fn, opts) {
|
|
5048
5319
|
const type = opts?.type ?? "other";
|
|
5049
5320
|
const key = opts?.key;
|
|
@@ -5071,6 +5342,11 @@ var init_send_queue = __esm({
|
|
|
5071
5342
|
this.scheduleProcess();
|
|
5072
5343
|
return promise;
|
|
5073
5344
|
}
|
|
5345
|
+
/**
|
|
5346
|
+
* Called when a platform rate limit is hit. Drops all pending text items
|
|
5347
|
+
* (draft updates) to reduce backlog, keeping only non-text items that
|
|
5348
|
+
* represent important operations (e.g., permission requests).
|
|
5349
|
+
*/
|
|
5074
5350
|
onRateLimited() {
|
|
5075
5351
|
this.config.onRateLimited?.();
|
|
5076
5352
|
const remaining = [];
|
|
@@ -5089,6 +5365,10 @@ var init_send_queue = __esm({
|
|
|
5089
5365
|
}
|
|
5090
5366
|
this.items = [];
|
|
5091
5367
|
}
|
|
5368
|
+
/**
|
|
5369
|
+
* Schedules the next item for processing after the rate-limit delay.
|
|
5370
|
+
* Uses per-category timing when available, falling back to the global minInterval.
|
|
5371
|
+
*/
|
|
5092
5372
|
scheduleProcess() {
|
|
5093
5373
|
if (this.processing) return;
|
|
5094
5374
|
if (this.items.length === 0) return;
|
|
@@ -5147,6 +5427,9 @@ var init_tool_card_state = __esm({
|
|
|
5147
5427
|
specs = [];
|
|
5148
5428
|
planEntries;
|
|
5149
5429
|
usage;
|
|
5430
|
+
// Lifecycle: active (first flush pending) → active (subsequent updates debounced) → finalized.
|
|
5431
|
+
// Once finalized, all updateFromSpec/updatePlan/appendUsage/finalize() calls are no-ops —
|
|
5432
|
+
// guards against events arriving after the session has ended or the tool has already completed.
|
|
5150
5433
|
finalized = false;
|
|
5151
5434
|
isFirstFlush = true;
|
|
5152
5435
|
debounceTimer;
|
|
@@ -5154,6 +5437,7 @@ var init_tool_card_state = __esm({
|
|
|
5154
5437
|
constructor(config) {
|
|
5155
5438
|
this.onFlush = config.onFlush;
|
|
5156
5439
|
}
|
|
5440
|
+
/** Adds or updates a tool spec. First call flushes immediately; subsequent calls are debounced. */
|
|
5157
5441
|
updateFromSpec(spec) {
|
|
5158
5442
|
if (this.finalized) return;
|
|
5159
5443
|
const existingIdx = this.specs.findIndex((s) => s.id === spec.id);
|
|
@@ -5169,6 +5453,7 @@ var init_tool_card_state = __esm({
|
|
|
5169
5453
|
this.scheduleFlush();
|
|
5170
5454
|
}
|
|
5171
5455
|
}
|
|
5456
|
+
/** Updates the plan entries displayed alongside tool cards. */
|
|
5172
5457
|
updatePlan(entries) {
|
|
5173
5458
|
if (this.finalized) return;
|
|
5174
5459
|
this.planEntries = entries;
|
|
@@ -5179,17 +5464,20 @@ var init_tool_card_state = __esm({
|
|
|
5179
5464
|
this.scheduleFlush();
|
|
5180
5465
|
}
|
|
5181
5466
|
}
|
|
5467
|
+
/** Appends token usage data to the tool card (typically at end of turn). */
|
|
5182
5468
|
appendUsage(usage) {
|
|
5183
5469
|
if (this.finalized) return;
|
|
5184
5470
|
this.usage = usage;
|
|
5185
5471
|
this.scheduleFlush();
|
|
5186
5472
|
}
|
|
5473
|
+
/** Marks the turn as complete and flushes the final snapshot immediately. */
|
|
5187
5474
|
finalize() {
|
|
5188
5475
|
if (this.finalized) return;
|
|
5189
5476
|
this.finalized = true;
|
|
5190
5477
|
this.clearDebounce();
|
|
5191
5478
|
this.flush();
|
|
5192
5479
|
}
|
|
5480
|
+
/** Stops all pending flushes without emitting a final snapshot. */
|
|
5193
5481
|
destroy() {
|
|
5194
5482
|
this.finalized = true;
|
|
5195
5483
|
this.clearDebounce();
|
|
@@ -5295,9 +5583,11 @@ var init_stream_accumulator = __esm({
|
|
|
5295
5583
|
entry.diffStats = update.diffStats;
|
|
5296
5584
|
}
|
|
5297
5585
|
}
|
|
5586
|
+
/** Retrieves a tool entry by ID, or undefined if not yet tracked. */
|
|
5298
5587
|
get(id) {
|
|
5299
5588
|
return this.entries.get(id);
|
|
5300
5589
|
}
|
|
5590
|
+
/** Resets all state between turns. */
|
|
5301
5591
|
clear() {
|
|
5302
5592
|
this.entries.clear();
|
|
5303
5593
|
this.pendingUpdates.clear();
|
|
@@ -5306,20 +5596,24 @@ var init_stream_accumulator = __esm({
|
|
|
5306
5596
|
ThoughtBuffer = class {
|
|
5307
5597
|
chunks = [];
|
|
5308
5598
|
sealed = false;
|
|
5599
|
+
/** Appends a thought text chunk. Ignored if already sealed. */
|
|
5309
5600
|
append(chunk) {
|
|
5310
5601
|
if (this.sealed) return;
|
|
5311
5602
|
this.chunks.push(chunk);
|
|
5312
5603
|
}
|
|
5604
|
+
/** Marks the thought as complete and returns the full accumulated text. */
|
|
5313
5605
|
seal() {
|
|
5314
5606
|
this.sealed = true;
|
|
5315
5607
|
return this.chunks.join("");
|
|
5316
5608
|
}
|
|
5609
|
+
/** Returns the text accumulated so far without sealing. */
|
|
5317
5610
|
getText() {
|
|
5318
5611
|
return this.chunks.join("");
|
|
5319
5612
|
}
|
|
5320
5613
|
isSealed() {
|
|
5321
5614
|
return this.sealed;
|
|
5322
5615
|
}
|
|
5616
|
+
/** Resets the buffer for reuse in a new turn. */
|
|
5323
5617
|
reset() {
|
|
5324
5618
|
this.chunks = [];
|
|
5325
5619
|
this.sealed = false;
|
|
@@ -5472,6 +5766,13 @@ var init_display_spec_builder = __esm({
|
|
|
5472
5766
|
constructor(tunnelService) {
|
|
5473
5767
|
this.tunnelService = tunnelService;
|
|
5474
5768
|
}
|
|
5769
|
+
/**
|
|
5770
|
+
* Builds a display spec for a single tool call entry.
|
|
5771
|
+
*
|
|
5772
|
+
* Deduplicates fields to avoid repeating the same info (e.g., if the title
|
|
5773
|
+
* was derived from the command, the command field is omitted). For long
|
|
5774
|
+
* output, generates a viewer link via the tunnel service when available.
|
|
5775
|
+
*/
|
|
5475
5776
|
buildToolSpec(entry, mode, sessionContext) {
|
|
5476
5777
|
const effectiveKind = entry.displayKind ?? (isApplyPatchOtherTool(entry.kind, entry.name, entry.rawInput) ? "edit" : entry.kind);
|
|
5477
5778
|
const icon = KIND_ICONS[effectiveKind] ?? KIND_ICONS["other"] ?? "\u{1F6E0}\uFE0F";
|
|
@@ -5530,6 +5831,7 @@ var init_display_spec_builder = __esm({
|
|
|
5530
5831
|
isHidden
|
|
5531
5832
|
};
|
|
5532
5833
|
}
|
|
5834
|
+
/** Builds a display spec for an agent thought. Content is only included at high verbosity. */
|
|
5533
5835
|
buildThoughtSpec(content, mode) {
|
|
5534
5836
|
const indicator = "Thinking...";
|
|
5535
5837
|
return {
|
|
@@ -5551,6 +5853,7 @@ var init_output_mode_resolver = __esm({
|
|
|
5551
5853
|
"use strict";
|
|
5552
5854
|
VALID_MODES = /* @__PURE__ */ new Set(["low", "medium", "high"]);
|
|
5553
5855
|
OutputModeResolver = class {
|
|
5856
|
+
/** Resolves the effective output mode by walking the override cascade. */
|
|
5554
5857
|
resolve(configManager, adapterName, sessionId, sessionManager) {
|
|
5555
5858
|
const config = configManager.get();
|
|
5556
5859
|
let mode = toOutputMode(config.outputMode) ?? "medium";
|
|
@@ -8425,6 +8728,14 @@ var init_permissions = __esm({
|
|
|
8425
8728
|
this.sendNotification = sendNotification;
|
|
8426
8729
|
}
|
|
8427
8730
|
pending = /* @__PURE__ */ new Map();
|
|
8731
|
+
/**
|
|
8732
|
+
* Send a permission request to the session's topic as an inline keyboard message,
|
|
8733
|
+
* and fire a notification to the Notifications topic so the user is alerted.
|
|
8734
|
+
*
|
|
8735
|
+
* Each button encodes `p:<callbackKey>:<optionId>`. The callbackKey is a short
|
|
8736
|
+
* nanoid that maps to the full pending state stored in-memory, avoiding the
|
|
8737
|
+
* 64-byte Telegram callback_data limit.
|
|
8738
|
+
*/
|
|
8428
8739
|
async sendPermissionRequest(session, request) {
|
|
8429
8740
|
const threadId = Number(session.threadId);
|
|
8430
8741
|
const callbackKey = nanoid4(8);
|
|
@@ -8459,6 +8770,12 @@ ${escapeHtml(request.description)}`,
|
|
|
8459
8770
|
deepLink
|
|
8460
8771
|
});
|
|
8461
8772
|
}
|
|
8773
|
+
/**
|
|
8774
|
+
* Register the `p:` callback handler in the bot's middleware chain.
|
|
8775
|
+
*
|
|
8776
|
+
* Must be called during setup so grammY processes permission responses
|
|
8777
|
+
* before other generic callback handlers.
|
|
8778
|
+
*/
|
|
8462
8779
|
setupCallbackHandler() {
|
|
8463
8780
|
this.bot.on("callback_query:data", async (ctx, next) => {
|
|
8464
8781
|
const data = ctx.callbackQuery.data;
|
|
@@ -8617,7 +8934,10 @@ var init_activity = __esm({
|
|
|
8617
8934
|
this.sessionId = sessionId;
|
|
8618
8935
|
this.state = new ToolCardState({
|
|
8619
8936
|
onFlush: (snapshot) => {
|
|
8620
|
-
this.flushPromise = this.flushPromise.then(() =>
|
|
8937
|
+
this.flushPromise = this.flushPromise.then(() => {
|
|
8938
|
+
if (this.aborted) return;
|
|
8939
|
+
return this._sendOrEdit(snapshot);
|
|
8940
|
+
}).catch(() => {
|
|
8621
8941
|
});
|
|
8622
8942
|
}
|
|
8623
8943
|
});
|
|
@@ -8627,6 +8947,7 @@ var init_activity = __esm({
|
|
|
8627
8947
|
lastSentText;
|
|
8628
8948
|
flushPromise = Promise.resolve();
|
|
8629
8949
|
overflowMsgIds = [];
|
|
8950
|
+
aborted = false;
|
|
8630
8951
|
tracer;
|
|
8631
8952
|
sessionId;
|
|
8632
8953
|
updateFromSpec(spec) {
|
|
@@ -8643,6 +8964,7 @@ var init_activity = __esm({
|
|
|
8643
8964
|
await this.flushPromise;
|
|
8644
8965
|
}
|
|
8645
8966
|
destroy() {
|
|
8967
|
+
this.aborted = true;
|
|
8646
8968
|
this.state.destroy();
|
|
8647
8969
|
}
|
|
8648
8970
|
hasContent() {
|
|
@@ -8883,6 +9205,7 @@ var init_streaming = __esm({
|
|
|
8883
9205
|
lastSentHtml = "";
|
|
8884
9206
|
displayTruncated = false;
|
|
8885
9207
|
tracer;
|
|
9208
|
+
/** Append a text chunk to the buffer and schedule a throttled flush. */
|
|
8886
9209
|
append(text3) {
|
|
8887
9210
|
if (!text3) return;
|
|
8888
9211
|
this.buffer += text3;
|
|
@@ -8962,6 +9285,16 @@ var init_streaming = __esm({
|
|
|
8962
9285
|
}
|
|
8963
9286
|
}
|
|
8964
9287
|
}
|
|
9288
|
+
/**
|
|
9289
|
+
* Flush the complete buffer as the final message for this turn.
|
|
9290
|
+
*
|
|
9291
|
+
* Cancels any pending debounce timer, waits for in-flight flushes to settle,
|
|
9292
|
+
* then sends the full content. If the HTML exceeds 4096 bytes, the content is
|
|
9293
|
+
* split at markdown boundaries to avoid breaking HTML tags mid-tag.
|
|
9294
|
+
*
|
|
9295
|
+
* Returns the Telegram message ID of the sent/edited message, or undefined
|
|
9296
|
+
* if nothing was sent (empty buffer or all network calls failed).
|
|
9297
|
+
*/
|
|
8965
9298
|
async finalize() {
|
|
8966
9299
|
this.tracer?.log("telegram", { action: "draft:finalize", sessionId: this.sessionId, bufferLen: this.buffer.length, msgId: this.messageId });
|
|
8967
9300
|
if (this.flushTimer) {
|
|
@@ -9033,9 +9366,14 @@ var init_streaming = __esm({
|
|
|
9033
9366
|
this.tracer?.log("telegram", { action: "draft:finalize:split", sessionId: this.sessionId, chunks: mdChunks.length });
|
|
9034
9367
|
return this.messageId;
|
|
9035
9368
|
}
|
|
9369
|
+
/** Returns the Telegram message ID for this draft, or undefined if not yet sent. */
|
|
9036
9370
|
getMessageId() {
|
|
9037
9371
|
return this.messageId;
|
|
9038
9372
|
}
|
|
9373
|
+
/**
|
|
9374
|
+
* Strip occurrences of `pattern` from the buffer and edit the message in-place.
|
|
9375
|
+
* Used by the TTS plugin to remove [TTS]...[/TTS] blocks after audio is sent.
|
|
9376
|
+
*/
|
|
9039
9377
|
async stripPattern(pattern) {
|
|
9040
9378
|
if (!this.messageId || !this.buffer) return;
|
|
9041
9379
|
const stripped = this.buffer.replace(pattern, "").trim();
|
|
@@ -9073,6 +9411,10 @@ var init_draft_manager = __esm({
|
|
|
9073
9411
|
drafts = /* @__PURE__ */ new Map();
|
|
9074
9412
|
textBuffers = /* @__PURE__ */ new Map();
|
|
9075
9413
|
finalizedDrafts = /* @__PURE__ */ new Map();
|
|
9414
|
+
/**
|
|
9415
|
+
* Return the active draft for a session, creating one if it doesn't exist yet.
|
|
9416
|
+
* Only one draft per session exists at a time.
|
|
9417
|
+
*/
|
|
9076
9418
|
getOrCreate(sessionId, threadId, tracer = null) {
|
|
9077
9419
|
let draft = this.drafts.get(sessionId);
|
|
9078
9420
|
if (!draft) {
|
|
@@ -9101,7 +9443,12 @@ var init_draft_manager = __esm({
|
|
|
9101
9443
|
);
|
|
9102
9444
|
}
|
|
9103
9445
|
/**
|
|
9104
|
-
* Finalize the
|
|
9446
|
+
* Finalize the active draft for a session and retain a short-lived reference for post-send edits.
|
|
9447
|
+
*
|
|
9448
|
+
* Removes the draft from the active map before awaiting to prevent concurrent calls from
|
|
9449
|
+
* double-finalizing the same draft. If the draft produces a message ID, stores it as a
|
|
9450
|
+
* `FinalizedDraft` so `stripPattern` (e.g. TTS block removal) can still edit the message
|
|
9451
|
+
* after it has been sent.
|
|
9105
9452
|
*/
|
|
9106
9453
|
async finalize(sessionId, _assistantSessionId) {
|
|
9107
9454
|
const draft = this.drafts.get(sessionId);
|
|
@@ -9128,6 +9475,12 @@ var init_draft_manager = __esm({
|
|
|
9128
9475
|
await finalized.draft.stripPattern(pattern);
|
|
9129
9476
|
}
|
|
9130
9477
|
}
|
|
9478
|
+
/**
|
|
9479
|
+
* Discard all draft state for a session without sending anything.
|
|
9480
|
+
*
|
|
9481
|
+
* Removes the active draft, text buffer, and finalized draft reference. Called when a
|
|
9482
|
+
* session ends or is reset and any unsent content should be silently dropped.
|
|
9483
|
+
*/
|
|
9131
9484
|
cleanup(sessionId) {
|
|
9132
9485
|
this.drafts.delete(sessionId);
|
|
9133
9486
|
this.textBuffers.delete(sessionId);
|
|
@@ -9146,14 +9499,19 @@ var init_skill_command_manager = __esm({
|
|
|
9146
9499
|
init_log();
|
|
9147
9500
|
log34 = createChildLogger({ module: "skill-commands" });
|
|
9148
9501
|
SkillCommandManager = class {
|
|
9149
|
-
// sessionId → pinned msgId
|
|
9150
9502
|
constructor(bot, chatId, sendQueue, sessionManager) {
|
|
9151
9503
|
this.bot = bot;
|
|
9152
9504
|
this.chatId = chatId;
|
|
9153
9505
|
this.sendQueue = sendQueue;
|
|
9154
9506
|
this.sessionManager = sessionManager;
|
|
9155
9507
|
}
|
|
9508
|
+
// sessionId → Telegram message ID of the pinned skills message
|
|
9156
9509
|
messages = /* @__PURE__ */ new Map();
|
|
9510
|
+
/**
|
|
9511
|
+
* Send or update the pinned skill commands message for a session.
|
|
9512
|
+
* Creates a new pinned message if none exists; edits the existing one otherwise.
|
|
9513
|
+
* Passing an empty `commands` array removes the pinned message.
|
|
9514
|
+
*/
|
|
9157
9515
|
async send(sessionId, threadId, commands) {
|
|
9158
9516
|
if (!this.messages.has(sessionId)) {
|
|
9159
9517
|
const record = this.sessionManager.getSessionRecord(sessionId);
|
|
@@ -9534,7 +9892,11 @@ var init_adapter = __esm({
|
|
|
9534
9892
|
_topicsInitialized = false;
|
|
9535
9893
|
/** Background watcher timer — cancelled on stop() or when topics succeed */
|
|
9536
9894
|
_prerequisiteWatcher = null;
|
|
9537
|
-
/**
|
|
9895
|
+
/**
|
|
9896
|
+
* Persist the control message ID both in-memory and to the session record.
|
|
9897
|
+
* The control message is the pinned status card with bypass/TTS buttons; its ID
|
|
9898
|
+
* is needed after a restart to edit it when config changes.
|
|
9899
|
+
*/
|
|
9538
9900
|
storeControlMsgId(sessionId, msgId) {
|
|
9539
9901
|
this.controlMsgIds.set(sessionId, msgId);
|
|
9540
9902
|
const record = this.core.sessionManager.getSessionRecord(sessionId);
|
|
@@ -9599,6 +9961,12 @@ var init_adapter = __esm({
|
|
|
9599
9961
|
this.telegramConfig = config;
|
|
9600
9962
|
this.saveTopicIds = saveTopicIds;
|
|
9601
9963
|
}
|
|
9964
|
+
/**
|
|
9965
|
+
* Set up the grammY bot, register all callback and message handlers, then perform
|
|
9966
|
+
* two-phase startup: Phase 1 starts polling immediately; Phase 2 checks group
|
|
9967
|
+
* prerequisites (bot is admin, topics are enabled) and creates/restores system topics.
|
|
9968
|
+
* If prerequisites are not met, a background watcher retries until they are.
|
|
9969
|
+
*/
|
|
9602
9970
|
async start() {
|
|
9603
9971
|
this.bot = new Bot(this.telegramConfig.botToken, {
|
|
9604
9972
|
client: {
|
|
@@ -10029,6 +10397,13 @@ OpenACP will automatically retry until this is resolved.`;
|
|
|
10029
10397
|
};
|
|
10030
10398
|
this._prerequisiteWatcher = setTimeout(retry, schedule[0]);
|
|
10031
10399
|
}
|
|
10400
|
+
/**
|
|
10401
|
+
* Tear down the bot and release all associated resources.
|
|
10402
|
+
*
|
|
10403
|
+
* Cancels the background prerequisite watcher, destroys all per-session activity
|
|
10404
|
+
* trackers (which hold interval timers), removes eventBus listeners, clears the
|
|
10405
|
+
* send queue, and stops the grammY bot polling loop.
|
|
10406
|
+
*/
|
|
10032
10407
|
async stop() {
|
|
10033
10408
|
if (this._prerequisiteWatcher !== null) {
|
|
10034
10409
|
clearTimeout(this._prerequisiteWatcher);
|
|
@@ -10162,8 +10537,7 @@ ${lines.join("\n")}`;
|
|
|
10162
10537
|
await this.draftManager.finalize(sessionId, assistantSession?.id);
|
|
10163
10538
|
}
|
|
10164
10539
|
if (sessionId) {
|
|
10165
|
-
|
|
10166
|
-
if (tracker) await tracker.onNewPrompt();
|
|
10540
|
+
await this.drainAndResetTracker(sessionId);
|
|
10167
10541
|
}
|
|
10168
10542
|
ctx.replyWithChatAction("typing").catch(() => {
|
|
10169
10543
|
});
|
|
@@ -10252,9 +10626,26 @@ ${lines.join("\n")}`;
|
|
|
10252
10626
|
* its creation event. This queue ensures events are processed in the order they arrive.
|
|
10253
10627
|
*/
|
|
10254
10628
|
_dispatchQueues = /* @__PURE__ */ new Map();
|
|
10629
|
+
/**
|
|
10630
|
+
* Drain pending event dispatches from the previous prompt, then reset the
|
|
10631
|
+
* activity tracker so late tool_call events don't leak into the new card.
|
|
10632
|
+
*/
|
|
10633
|
+
async drainAndResetTracker(sessionId) {
|
|
10634
|
+
const pendingDispatch = this._dispatchQueues.get(sessionId);
|
|
10635
|
+
if (pendingDispatch) await pendingDispatch;
|
|
10636
|
+
const tracker = this.sessionTrackers.get(sessionId);
|
|
10637
|
+
if (tracker) await tracker.onNewPrompt();
|
|
10638
|
+
}
|
|
10255
10639
|
getTracer(sessionId) {
|
|
10256
10640
|
return this.core.sessionManager.getSession(sessionId)?.agentInstance?.debugTracer ?? null;
|
|
10257
10641
|
}
|
|
10642
|
+
/**
|
|
10643
|
+
* Primary outbound dispatch method — routes an agent message to the session's Telegram topic.
|
|
10644
|
+
*
|
|
10645
|
+
* Wraps the base class `sendMessage` in a per-session promise chain (`_dispatchQueues`)
|
|
10646
|
+
* so that concurrent events fired from SessionBridge are serialized and delivered in the
|
|
10647
|
+
* order they arrive, preventing fast handlers from overtaking slower ones.
|
|
10648
|
+
*/
|
|
10258
10649
|
async sendMessage(sessionId, content) {
|
|
10259
10650
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
10260
10651
|
if (!session) return;
|
|
@@ -10568,6 +10959,11 @@ Task completed.
|
|
|
10568
10959
|
)
|
|
10569
10960
|
);
|
|
10570
10961
|
}
|
|
10962
|
+
/**
|
|
10963
|
+
* Render a PermissionRequest as an inline keyboard in the session topic and
|
|
10964
|
+
* notify the Notifications topic. Runs inside a sendQueue item, so
|
|
10965
|
+
* notification is fire-and-forget to avoid deadlock.
|
|
10966
|
+
*/
|
|
10571
10967
|
async sendPermissionRequest(sessionId, request) {
|
|
10572
10968
|
this.getTracer(sessionId)?.log("telegram", { action: "permission:send", sessionId, requestId: request.id, description: request.description });
|
|
10573
10969
|
log36.info({ sessionId, requestId: request.id }, "Permission request sent");
|
|
@@ -10577,6 +10973,11 @@ Task completed.
|
|
|
10577
10973
|
() => this.permissionHandler.sendPermissionRequest(session, request)
|
|
10578
10974
|
);
|
|
10579
10975
|
}
|
|
10976
|
+
/**
|
|
10977
|
+
* Post a notification to the Notifications topic.
|
|
10978
|
+
* Assistant session notifications are suppressed — the assistant topic is
|
|
10979
|
+
* the user's primary interface and does not need a separate alert.
|
|
10980
|
+
*/
|
|
10580
10981
|
async sendNotification(notification) {
|
|
10581
10982
|
this.getTracer(notification.sessionId)?.log("telegram", { action: "notification:send", sessionId: notification.sessionId, type: notification.type });
|
|
10582
10983
|
if (notification.sessionId === this.core.assistantManager?.get("telegram")?.id) return;
|
|
@@ -10617,6 +11018,10 @@ Task completed.
|
|
|
10617
11018
|
})
|
|
10618
11019
|
);
|
|
10619
11020
|
}
|
|
11021
|
+
/**
|
|
11022
|
+
* Create a new Telegram forum topic for a session and return its thread ID as a string.
|
|
11023
|
+
* Called by the core when a session is created via the API or CLI (not from the Telegram UI).
|
|
11024
|
+
*/
|
|
10620
11025
|
async createSessionThread(sessionId, name) {
|
|
10621
11026
|
this.getTracer(sessionId)?.log("telegram", { action: "thread:create", sessionId, name });
|
|
10622
11027
|
log36.info({ sessionId, name }, "Session topic created");
|
|
@@ -10624,6 +11029,10 @@ Task completed.
|
|
|
10624
11029
|
await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
|
|
10625
11030
|
);
|
|
10626
11031
|
}
|
|
11032
|
+
/**
|
|
11033
|
+
* Rename the forum topic for a session and update the session record's display name.
|
|
11034
|
+
* No-ops silently if the session doesn't have a threadId yet (e.g. still initializing).
|
|
11035
|
+
*/
|
|
10627
11036
|
async renameSessionThread(sessionId, newName) {
|
|
10628
11037
|
this.getTracer(sessionId)?.log("telegram", { action: "thread:rename", sessionId, newName });
|
|
10629
11038
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
@@ -10641,6 +11050,7 @@ Task completed.
|
|
|
10641
11050
|
);
|
|
10642
11051
|
await this.core.sessionManager.patchRecord(sessionId, { name: newName });
|
|
10643
11052
|
}
|
|
11053
|
+
/** Delete the forum topic associated with a session. */
|
|
10644
11054
|
async deleteSessionThread(sessionId) {
|
|
10645
11055
|
const record = this.core.sessionManager.getSessionRecord(sessionId);
|
|
10646
11056
|
const platform2 = record?.platform;
|
|
@@ -10655,6 +11065,11 @@ Task completed.
|
|
|
10655
11065
|
);
|
|
10656
11066
|
}
|
|
10657
11067
|
}
|
|
11068
|
+
/**
|
|
11069
|
+
* Display or update the pinned skill commands message for a session.
|
|
11070
|
+
* If the session's threadId is not yet set (e.g. session created from API),
|
|
11071
|
+
* the commands are queued and flushed once the thread becomes available.
|
|
11072
|
+
*/
|
|
10658
11073
|
async sendSkillCommands(sessionId, commands) {
|
|
10659
11074
|
if (sessionId === this.core.assistantManager?.get("telegram")?.id) return;
|
|
10660
11075
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
@@ -10739,7 +11154,10 @@ Task completed.
|
|
|
10739
11154
|
return;
|
|
10740
11155
|
}
|
|
10741
11156
|
const sid = await this.resolveSessionId(threadId);
|
|
10742
|
-
if (sid)
|
|
11157
|
+
if (sid) {
|
|
11158
|
+
await this.draftManager.finalize(sid, this.core.assistantManager?.get("telegram")?.id);
|
|
11159
|
+
await this.drainAndResetTracker(sid);
|
|
11160
|
+
}
|
|
10743
11161
|
this.core.handleMessage({
|
|
10744
11162
|
channelId: "telegram",
|
|
10745
11163
|
threadId: String(threadId),
|
|
@@ -10748,10 +11166,24 @@ Task completed.
|
|
|
10748
11166
|
attachments: [att]
|
|
10749
11167
|
}).catch((err) => log36.error({ err }, "handleMessage error"));
|
|
10750
11168
|
}
|
|
11169
|
+
/**
|
|
11170
|
+
* Remove skill slash commands from the Telegram bot command list for a session.
|
|
11171
|
+
*
|
|
11172
|
+
* Clears any queued pending commands that hadn't been sent yet, then delegates
|
|
11173
|
+
* to `SkillCommandManager` to delete the commands from the Telegram API. Called
|
|
11174
|
+
* when a session with registered skill commands ends.
|
|
11175
|
+
*/
|
|
10751
11176
|
async cleanupSkillCommands(sessionId) {
|
|
10752
11177
|
this._pendingSkillCommands.delete(sessionId);
|
|
10753
11178
|
await this.skillManager.cleanup(sessionId);
|
|
10754
11179
|
}
|
|
11180
|
+
/**
|
|
11181
|
+
* Clean up all adapter state associated with a session.
|
|
11182
|
+
*
|
|
11183
|
+
* Finalizes and discards any in-flight draft, destroys the activity tracker
|
|
11184
|
+
* (stopping ThinkingIndicator timers and finalizing any open ToolCard), and
|
|
11185
|
+
* clears pending skill commands. Called when a session ends or is reset.
|
|
11186
|
+
*/
|
|
10755
11187
|
async cleanupSessionState(sessionId) {
|
|
10756
11188
|
this._pendingSkillCommands.delete(sessionId);
|
|
10757
11189
|
await this.draftManager.finalize(sessionId, this.core.assistantManager?.get("telegram")?.id);
|
|
@@ -10762,9 +11194,22 @@ Task completed.
|
|
|
10762
11194
|
this.sessionTrackers.delete(sessionId);
|
|
10763
11195
|
}
|
|
10764
11196
|
}
|
|
11197
|
+
/**
|
|
11198
|
+
* Remove `[TTS]...[/TTS]` blocks from the active or finalized draft for a session.
|
|
11199
|
+
*
|
|
11200
|
+
* The agent embeds these blocks so the speech plugin can extract the TTS text, but
|
|
11201
|
+
* they should never appear in the chat message. Called after TTS audio has been sent.
|
|
11202
|
+
*/
|
|
10765
11203
|
async stripTTSBlock(sessionId) {
|
|
10766
11204
|
await this.draftManager.stripPattern(sessionId, /\[TTS\][\s\S]*?\[\/TTS\]/g);
|
|
10767
11205
|
}
|
|
11206
|
+
/**
|
|
11207
|
+
* Archive a session by deleting its forum topic.
|
|
11208
|
+
*
|
|
11209
|
+
* Sets `session.archiving = true` to suppress any outgoing messages while the
|
|
11210
|
+
* topic is being torn down, finalizes pending drafts, cleans up all trackers,
|
|
11211
|
+
* then deletes the Telegram topic (which removes all messages).
|
|
11212
|
+
*/
|
|
10768
11213
|
async archiveSessionTopic(sessionId) {
|
|
10769
11214
|
this.getTracer(sessionId)?.log("telegram", { action: "thread:archive", sessionId });
|
|
10770
11215
|
const core = this.core;
|
|
@@ -10865,12 +11310,14 @@ var StderrCapture = class {
|
|
|
10865
11310
|
this.maxLines = maxLines;
|
|
10866
11311
|
}
|
|
10867
11312
|
lines = [];
|
|
11313
|
+
/** Append a chunk of stderr output, splitting on newlines and trimming to maxLines. */
|
|
10868
11314
|
append(chunk) {
|
|
10869
11315
|
this.lines.push(...chunk.split("\n").filter(Boolean));
|
|
10870
11316
|
if (this.lines.length > this.maxLines) {
|
|
10871
11317
|
this.lines = this.lines.slice(-this.maxLines);
|
|
10872
11318
|
}
|
|
10873
11319
|
}
|
|
11320
|
+
/** Return all captured lines joined as a single string. */
|
|
10874
11321
|
getLastLines() {
|
|
10875
11322
|
return this.lines.join("\n");
|
|
10876
11323
|
}
|
|
@@ -10891,13 +11338,18 @@ import fs4 from "fs";
|
|
|
10891
11338
|
import path4 from "path";
|
|
10892
11339
|
import ignore from "ignore";
|
|
10893
11340
|
var DEFAULT_DENY_PATTERNS = [
|
|
11341
|
+
// Environment files — contain API keys, database URLs, etc.
|
|
10894
11342
|
".env",
|
|
10895
11343
|
".env.*",
|
|
11344
|
+
// Cryptographic keys
|
|
10896
11345
|
"*.key",
|
|
10897
11346
|
"*.pem",
|
|
11347
|
+
// SSH and cloud credentials
|
|
10898
11348
|
".ssh/",
|
|
10899
11349
|
".aws/",
|
|
11350
|
+
// OpenACP workspace — contains bot tokens and secrets
|
|
10900
11351
|
".openacp/",
|
|
11352
|
+
// Generic credential/secret files
|
|
10901
11353
|
"**/credentials*",
|
|
10902
11354
|
"**/secrets*",
|
|
10903
11355
|
"**/*.secret"
|
|
@@ -10925,6 +11377,20 @@ var PathGuard = class {
|
|
|
10925
11377
|
this.ig.add(options.ignorePatterns);
|
|
10926
11378
|
}
|
|
10927
11379
|
}
|
|
11380
|
+
/**
|
|
11381
|
+
* Checks whether an agent is allowed to access the given path.
|
|
11382
|
+
*
|
|
11383
|
+
* Validation order:
|
|
11384
|
+
* 1. Write to .openacpignore is always blocked (prevents agents from weakening their own restrictions)
|
|
11385
|
+
* 2. Path must be within cwd or an explicitly allowlisted path
|
|
11386
|
+
* 3. If within cwd but not allowlisted, path must not match any deny pattern
|
|
11387
|
+
*
|
|
11388
|
+
* @param targetPath - The path the agent is attempting to access.
|
|
11389
|
+
* @param operation - The operation type. Write operations are subject to stricter
|
|
11390
|
+
* restrictions than reads — specifically, writing to `.openacpignore` is blocked
|
|
11391
|
+
* (to prevent agents from weakening their own restrictions), while reading it is allowed.
|
|
11392
|
+
* @returns `{ allowed: true }` or `{ allowed: false, reason: "..." }`
|
|
11393
|
+
*/
|
|
10928
11394
|
validatePath(targetPath, operation) {
|
|
10929
11395
|
const resolved = path4.resolve(targetPath);
|
|
10930
11396
|
let realPath;
|
|
@@ -10960,6 +11426,7 @@ var PathGuard = class {
|
|
|
10960
11426
|
}
|
|
10961
11427
|
return { allowed: true, reason: "" };
|
|
10962
11428
|
}
|
|
11429
|
+
/** Adds an additional allowed path at runtime (e.g. for file-service uploads). */
|
|
10963
11430
|
addAllowedPath(p) {
|
|
10964
11431
|
try {
|
|
10965
11432
|
this.allowedPaths.push(fs4.realpathSync(path4.resolve(p)));
|
|
@@ -10967,6 +11434,10 @@ var PathGuard = class {
|
|
|
10967
11434
|
this.allowedPaths.push(path4.resolve(p));
|
|
10968
11435
|
}
|
|
10969
11436
|
}
|
|
11437
|
+
/**
|
|
11438
|
+
* Loads additional deny patterns from .openacpignore in the workspace root.
|
|
11439
|
+
* Follows .gitignore syntax — blank lines and lines starting with # are skipped.
|
|
11440
|
+
*/
|
|
10970
11441
|
static loadIgnoreFile(cwd) {
|
|
10971
11442
|
const ignorePath = path4.join(cwd, ".openacpignore");
|
|
10972
11443
|
try {
|
|
@@ -10980,6 +11451,7 @@ var PathGuard = class {
|
|
|
10980
11451
|
|
|
10981
11452
|
// src/core/security/env-filter.ts
|
|
10982
11453
|
var DEFAULT_ENV_WHITELIST = [
|
|
11454
|
+
// Shell basics — agents need these to resolve commands and write temp files
|
|
10983
11455
|
"PATH",
|
|
10984
11456
|
"HOME",
|
|
10985
11457
|
"SHELL",
|
|
@@ -10992,11 +11464,11 @@ var DEFAULT_ENV_WHITELIST = [
|
|
|
10992
11464
|
"XDG_*",
|
|
10993
11465
|
"NODE_ENV",
|
|
10994
11466
|
"EDITOR",
|
|
10995
|
-
// Git
|
|
11467
|
+
// Git — agents need git config and SSH access for code operations
|
|
10996
11468
|
"GIT_*",
|
|
10997
11469
|
"SSH_AUTH_SOCK",
|
|
10998
11470
|
"SSH_AGENT_PID",
|
|
10999
|
-
// Terminal rendering
|
|
11471
|
+
// Terminal rendering — ensures correct color output in agent responses
|
|
11000
11472
|
"COLORTERM",
|
|
11001
11473
|
"FORCE_COLOR",
|
|
11002
11474
|
"NO_COLOR",
|
|
@@ -11031,6 +11503,7 @@ var TypedEmitter = class _TypedEmitter {
|
|
|
11031
11503
|
listeners = /* @__PURE__ */ new Map();
|
|
11032
11504
|
paused = false;
|
|
11033
11505
|
buffer = [];
|
|
11506
|
+
/** Register a listener for the given event. Returns `this` for chaining. */
|
|
11034
11507
|
on(event, listener) {
|
|
11035
11508
|
let set = this.listeners.get(event);
|
|
11036
11509
|
if (!set) {
|
|
@@ -11040,10 +11513,17 @@ var TypedEmitter = class _TypedEmitter {
|
|
|
11040
11513
|
set.add(listener);
|
|
11041
11514
|
return this;
|
|
11042
11515
|
}
|
|
11516
|
+
/** Remove a specific listener for the given event. */
|
|
11043
11517
|
off(event, listener) {
|
|
11044
11518
|
this.listeners.get(event)?.delete(listener);
|
|
11045
11519
|
return this;
|
|
11046
11520
|
}
|
|
11521
|
+
/**
|
|
11522
|
+
* Emit an event to all registered listeners.
|
|
11523
|
+
*
|
|
11524
|
+
* When paused, events are buffered (up to MAX_BUFFER_SIZE) unless
|
|
11525
|
+
* the passthrough filter allows them through immediately.
|
|
11526
|
+
*/
|
|
11047
11527
|
emit(event, ...args) {
|
|
11048
11528
|
if (this.paused) {
|
|
11049
11529
|
if (this.passthroughFn?.(event, args)) {
|
|
@@ -11087,6 +11567,7 @@ var TypedEmitter = class _TypedEmitter {
|
|
|
11087
11567
|
get bufferSize() {
|
|
11088
11568
|
return this.buffer.length;
|
|
11089
11569
|
}
|
|
11570
|
+
/** Remove all listeners for a specific event, or all events if none specified. */
|
|
11090
11571
|
removeAllListeners(event) {
|
|
11091
11572
|
if (event) {
|
|
11092
11573
|
this.listeners.delete(event);
|
|
@@ -11094,6 +11575,7 @@ var TypedEmitter = class _TypedEmitter {
|
|
|
11094
11575
|
this.listeners.clear();
|
|
11095
11576
|
}
|
|
11096
11577
|
}
|
|
11578
|
+
/** Deliver an event to listeners, isolating errors so one broken listener doesn't break others. */
|
|
11097
11579
|
deliver(event, args) {
|
|
11098
11580
|
const set = this.listeners.get(event);
|
|
11099
11581
|
if (!set) return;
|
|
@@ -11158,6 +11640,11 @@ var TerminalManager = class {
|
|
|
11158
11640
|
constructor(maxOutputBytes = 1024 * 1024) {
|
|
11159
11641
|
this.maxOutputBytes = maxOutputBytes;
|
|
11160
11642
|
}
|
|
11643
|
+
/**
|
|
11644
|
+
* Spawn a new terminal process. Runs terminal:beforeCreate middleware first
|
|
11645
|
+
* (which can modify command/args/env or block creation entirely).
|
|
11646
|
+
* Returns a terminalId for subsequent output/wait/kill operations.
|
|
11647
|
+
*/
|
|
11161
11648
|
async createTerminal(sessionId, params, middlewareChain) {
|
|
11162
11649
|
let termCommand = params.command;
|
|
11163
11650
|
let termArgs = params.args ?? [];
|
|
@@ -11239,6 +11726,7 @@ var TerminalManager = class {
|
|
|
11239
11726
|
});
|
|
11240
11727
|
return { terminalId };
|
|
11241
11728
|
}
|
|
11729
|
+
/** Retrieve accumulated stdout/stderr output for a terminal. */
|
|
11242
11730
|
getOutput(terminalId) {
|
|
11243
11731
|
const state = this.terminals.get(terminalId);
|
|
11244
11732
|
if (!state) {
|
|
@@ -11253,6 +11741,7 @@ var TerminalManager = class {
|
|
|
11253
11741
|
} : void 0
|
|
11254
11742
|
};
|
|
11255
11743
|
}
|
|
11744
|
+
/** Block until the terminal process exits, returning exit code and signal. */
|
|
11256
11745
|
async waitForExit(terminalId) {
|
|
11257
11746
|
const state = this.terminals.get(terminalId);
|
|
11258
11747
|
if (!state) {
|
|
@@ -11276,6 +11765,7 @@ var TerminalManager = class {
|
|
|
11276
11765
|
}
|
|
11277
11766
|
});
|
|
11278
11767
|
}
|
|
11768
|
+
/** Send SIGTERM to a terminal process (graceful shutdown). */
|
|
11279
11769
|
kill(terminalId) {
|
|
11280
11770
|
const state = this.terminals.get(terminalId);
|
|
11281
11771
|
if (!state) {
|
|
@@ -11283,6 +11773,7 @@ var TerminalManager = class {
|
|
|
11283
11773
|
}
|
|
11284
11774
|
state.process.kill("SIGTERM");
|
|
11285
11775
|
}
|
|
11776
|
+
/** Force-kill (SIGKILL) and immediately remove a terminal from the registry. */
|
|
11286
11777
|
release(terminalId) {
|
|
11287
11778
|
const state = this.terminals.get(terminalId);
|
|
11288
11779
|
if (!state) {
|
|
@@ -11291,6 +11782,7 @@ var TerminalManager = class {
|
|
|
11291
11782
|
state.process.kill("SIGKILL");
|
|
11292
11783
|
this.terminals.delete(terminalId);
|
|
11293
11784
|
}
|
|
11785
|
+
/** Force-kill all terminals. Used during session/system teardown. */
|
|
11294
11786
|
destroyAll() {
|
|
11295
11787
|
for (const [, t] of this.terminals) {
|
|
11296
11788
|
t.process.kill("SIGKILL");
|
|
@@ -11322,6 +11814,12 @@ var DebugTracer = class {
|
|
|
11322
11814
|
}
|
|
11323
11815
|
dirCreated = false;
|
|
11324
11816
|
logDir;
|
|
11817
|
+
/**
|
|
11818
|
+
* Write a timestamped JSONL entry to the trace file for the given layer.
|
|
11819
|
+
*
|
|
11820
|
+
* Handles circular references gracefully and silently swallows errors —
|
|
11821
|
+
* debug logging must never crash the application.
|
|
11822
|
+
*/
|
|
11325
11823
|
log(layer, data) {
|
|
11326
11824
|
try {
|
|
11327
11825
|
if (!this.dirCreated) {
|
|
@@ -11442,9 +11940,13 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
|
|
|
11442
11940
|
connection;
|
|
11443
11941
|
child;
|
|
11444
11942
|
stderrCapture;
|
|
11943
|
+
/** Manages terminal subprocesses that agents can spawn for shell commands. */
|
|
11445
11944
|
terminalManager = new TerminalManager();
|
|
11945
|
+
/** Shared across all instances — resolves MCP server configs for ACP sessions. */
|
|
11446
11946
|
static mcpManager = new McpManager();
|
|
11947
|
+
/** Guards against emitting crash events during intentional shutdown. */
|
|
11447
11948
|
_destroying = false;
|
|
11949
|
+
/** Restricts agent file I/O to the workspace directory and explicitly allowed paths. */
|
|
11448
11950
|
pathGuard;
|
|
11449
11951
|
sessionId;
|
|
11450
11952
|
agentName;
|
|
@@ -11454,16 +11956,36 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
|
|
|
11454
11956
|
initialSessionResponse;
|
|
11455
11957
|
middlewareChain;
|
|
11456
11958
|
debugTracer = null;
|
|
11457
|
-
/**
|
|
11959
|
+
/**
|
|
11960
|
+
* Whitelist an additional filesystem path for agent read access.
|
|
11961
|
+
*
|
|
11962
|
+
* Called by SessionFactory to allow agents to read files outside the
|
|
11963
|
+
* workspace (e.g., the file-service upload directory for attachments).
|
|
11964
|
+
*/
|
|
11458
11965
|
addAllowedPath(p) {
|
|
11459
11966
|
this.pathGuard.addAllowedPath(p);
|
|
11460
11967
|
}
|
|
11461
|
-
// Callback — set by
|
|
11968
|
+
// Callback — set by Session/Core when wiring events. Returns the selected
|
|
11969
|
+
// permission option ID. Default no-op auto-selects the first option.
|
|
11462
11970
|
onPermissionRequest = async () => "";
|
|
11463
11971
|
constructor(agentName) {
|
|
11464
11972
|
super();
|
|
11465
11973
|
this.agentName = agentName;
|
|
11466
11974
|
}
|
|
11975
|
+
/**
|
|
11976
|
+
* Spawn the agent child process and complete the ACP protocol handshake.
|
|
11977
|
+
*
|
|
11978
|
+
* Steps:
|
|
11979
|
+
* 1. Resolve the agent command to a directly executable path
|
|
11980
|
+
* 2. Create a PathGuard scoped to the working directory
|
|
11981
|
+
* 3. Spawn the subprocess with a filtered environment (security: only whitelisted
|
|
11982
|
+
* env vars are passed to prevent leaking secrets like API keys)
|
|
11983
|
+
* 4. Wire stdin/stdout through debug-tracing Transform streams
|
|
11984
|
+
* 5. Convert Node streams → Web streams for the ACP SDK
|
|
11985
|
+
* 6. Perform the ACP `initialize` handshake and negotiate capabilities
|
|
11986
|
+
*
|
|
11987
|
+
* Does NOT create a session — callers must follow up with newSession or loadSession.
|
|
11988
|
+
*/
|
|
11467
11989
|
static async spawnSubprocess(agentDef, workingDirectory, allowedPaths = []) {
|
|
11468
11990
|
const instance = new _AgentInstance(agentDef.name);
|
|
11469
11991
|
const resolved = resolveAgentCommand(agentDef.command);
|
|
@@ -11562,6 +12084,13 @@ var AgentInstance = class _AgentInstance extends TypedEmitter {
|
|
|
11562
12084
|
);
|
|
11563
12085
|
return instance;
|
|
11564
12086
|
}
|
|
12087
|
+
/**
|
|
12088
|
+
* Monitor the subprocess for unexpected exits and emit error events.
|
|
12089
|
+
*
|
|
12090
|
+
* Distinguishes intentional shutdown (SIGTERM/SIGINT during destroy) from
|
|
12091
|
+
* crashes (non-zero exit code or unexpected signal). Crash events include
|
|
12092
|
+
* captured stderr output for diagnostic context.
|
|
12093
|
+
*/
|
|
11565
12094
|
setupCrashDetection() {
|
|
11566
12095
|
this.child.on("exit", (code, signal) => {
|
|
11567
12096
|
if (this._destroying) return;
|
|
@@ -11584,6 +12113,18 @@ ${stderr}`
|
|
|
11584
12113
|
log4.debug({ sessionId: this.sessionId }, "ACP connection closed");
|
|
11585
12114
|
});
|
|
11586
12115
|
}
|
|
12116
|
+
/**
|
|
12117
|
+
* Spawn a new agent subprocess and create a fresh ACP session.
|
|
12118
|
+
*
|
|
12119
|
+
* This is the primary entry point for starting an agent. It spawns the
|
|
12120
|
+
* subprocess, completes the ACP handshake, and calls `newSession` to
|
|
12121
|
+
* initialize the agent's working context (cwd, MCP servers).
|
|
12122
|
+
*
|
|
12123
|
+
* @param agentDef - Agent definition (command, args, env) from the catalog
|
|
12124
|
+
* @param workingDirectory - Workspace root the agent operates in
|
|
12125
|
+
* @param mcpServers - Optional MCP server configs to extend agent capabilities
|
|
12126
|
+
* @param allowedPaths - Extra filesystem paths the agent may access
|
|
12127
|
+
*/
|
|
11587
12128
|
static async spawn(agentDef, workingDirectory, mcpServers, allowedPaths) {
|
|
11588
12129
|
log4.debug(
|
|
11589
12130
|
{ agentName: agentDef.name, command: agentDef.command },
|
|
@@ -11617,6 +12158,15 @@ ${stderr}`
|
|
|
11617
12158
|
);
|
|
11618
12159
|
return instance;
|
|
11619
12160
|
}
|
|
12161
|
+
/**
|
|
12162
|
+
* Spawn a new subprocess and restore an existing agent session.
|
|
12163
|
+
*
|
|
12164
|
+
* Tries loadSession first (preferred, stable API), falls back to the
|
|
12165
|
+
* unstable resumeSession, and finally falls back to creating a brand-new
|
|
12166
|
+
* session if resume fails entirely (e.g., agent lost its state).
|
|
12167
|
+
*
|
|
12168
|
+
* @param agentSessionId - The agent-side session ID to restore
|
|
12169
|
+
*/
|
|
11620
12170
|
static async resume(agentDef, workingDirectory, agentSessionId, mcpServers, allowedPaths) {
|
|
11621
12171
|
log4.debug({ agentName: agentDef.name, agentSessionId }, "Resuming agent");
|
|
11622
12172
|
const spawnStart = Date.now();
|
|
@@ -11683,12 +12233,26 @@ ${stderr}`
|
|
|
11683
12233
|
instance.setupCrashDetection();
|
|
11684
12234
|
return instance;
|
|
11685
12235
|
}
|
|
11686
|
-
|
|
12236
|
+
/**
|
|
12237
|
+
* Build the ACP Client callback object.
|
|
12238
|
+
*
|
|
12239
|
+
* The ACP SDK invokes these callbacks when the agent sends notifications
|
|
12240
|
+
* or requests. Each callback maps an ACP protocol message to either:
|
|
12241
|
+
* - An internal AgentEvent (emitted for Session/adapters to consume)
|
|
12242
|
+
* - A filesystem or terminal operation (executed on the agent's behalf)
|
|
12243
|
+
* - A permission request (proxied to the user via the adapter)
|
|
12244
|
+
*/
|
|
11687
12245
|
createClient(_agent) {
|
|
11688
12246
|
const self = this;
|
|
11689
12247
|
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
11690
12248
|
return {
|
|
11691
12249
|
// ── Session updates ──────────────────────────────────────────────────
|
|
12250
|
+
// The agent streams its response as a series of session update events.
|
|
12251
|
+
// Each event type maps to an internal AgentEvent that Session relays
|
|
12252
|
+
// to adapters for rendering (text chunks, tool calls, usage stats, etc.).
|
|
12253
|
+
// Chunks are forwarded to Session individually as they arrive — no buffering
|
|
12254
|
+
// happens at this layer. If buffering is needed (e.g., to avoid rate limits),
|
|
12255
|
+
// it is the responsibility of the Session or adapter layer.
|
|
11692
12256
|
async sessionUpdate(params) {
|
|
11693
12257
|
const update = params.update;
|
|
11694
12258
|
let event = null;
|
|
@@ -11817,6 +12381,10 @@ ${stderr}`
|
|
|
11817
12381
|
}
|
|
11818
12382
|
},
|
|
11819
12383
|
// ── Permission requests ──────────────────────────────────────────────
|
|
12384
|
+
// The agent needs user approval before performing sensitive operations
|
|
12385
|
+
// (e.g., file writes, shell commands). This proxies the request up
|
|
12386
|
+
// through Session → PermissionGate → adapter → user, then returns
|
|
12387
|
+
// the user's chosen option ID back to the agent.
|
|
11820
12388
|
async requestPermission(params) {
|
|
11821
12389
|
const permissionRequest = {
|
|
11822
12390
|
id: params.toolCall.toolCallId,
|
|
@@ -11833,6 +12401,9 @@ ${stderr}`
|
|
|
11833
12401
|
};
|
|
11834
12402
|
},
|
|
11835
12403
|
// ── File operations ──────────────────────────────────────────────────
|
|
12404
|
+
// The agent reads/writes files through these callbacks rather than
|
|
12405
|
+
// accessing the filesystem directly. This allows PathGuard to enforce
|
|
12406
|
+
// workspace boundaries and middleware hooks to intercept I/O.
|
|
11836
12407
|
async readTextFile(params) {
|
|
11837
12408
|
const p = params;
|
|
11838
12409
|
const pathCheck = self.pathGuard.validatePath(p.path, "read");
|
|
@@ -11868,6 +12439,8 @@ ${stderr}`
|
|
|
11868
12439
|
return {};
|
|
11869
12440
|
},
|
|
11870
12441
|
// ── Terminal operations (delegated to TerminalManager) ─────────────
|
|
12442
|
+
// Agents can spawn shell commands via terminal operations. TerminalManager
|
|
12443
|
+
// handles subprocess lifecycle, output capture, and byte-limit enforcement.
|
|
11871
12444
|
async createTerminal(params) {
|
|
11872
12445
|
return self.terminalManager.createTerminal(
|
|
11873
12446
|
self.sessionId,
|
|
@@ -11897,6 +12470,13 @@ ${stderr}`
|
|
|
11897
12470
|
};
|
|
11898
12471
|
}
|
|
11899
12472
|
// ── New ACP methods ──────────────────────────────────────────────────
|
|
12473
|
+
/**
|
|
12474
|
+
* Update a session config option (mode, model, etc.) on the agent.
|
|
12475
|
+
*
|
|
12476
|
+
* Falls back to legacy `setSessionMode`/`unstable_setSessionModel` methods
|
|
12477
|
+
* for agents that haven't adopted the unified `session/set_config_option`
|
|
12478
|
+
* ACP method (detected via JSON-RPC -32601 "Method Not Found" error).
|
|
12479
|
+
*/
|
|
11900
12480
|
async setConfigOption(configId, value) {
|
|
11901
12481
|
try {
|
|
11902
12482
|
return await this.connection.setSessionConfigOption({
|
|
@@ -11924,12 +12504,14 @@ ${stderr}`
|
|
|
11924
12504
|
throw err;
|
|
11925
12505
|
}
|
|
11926
12506
|
}
|
|
12507
|
+
/** List the agent's known sessions, optionally filtered by working directory. */
|
|
11927
12508
|
async listSessions(cwd, cursor) {
|
|
11928
12509
|
return await this.connection.listSessions({
|
|
11929
12510
|
cwd: cwd ?? null,
|
|
11930
12511
|
cursor: cursor ?? null
|
|
11931
12512
|
});
|
|
11932
12513
|
}
|
|
12514
|
+
/** Load an existing agent session by ID into this subprocess. */
|
|
11933
12515
|
async loadSession(sessionId, cwd, mcpServers) {
|
|
11934
12516
|
const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
|
|
11935
12517
|
return await this.connection.loadSession({
|
|
@@ -11938,9 +12520,11 @@ ${stderr}`
|
|
|
11938
12520
|
mcpServers: resolvedMcp
|
|
11939
12521
|
});
|
|
11940
12522
|
}
|
|
12523
|
+
/** Trigger agent-managed authentication (e.g., OAuth flow). */
|
|
11941
12524
|
async authenticate(methodId) {
|
|
11942
12525
|
await this.connection.authenticate({ methodId });
|
|
11943
12526
|
}
|
|
12527
|
+
/** Fork an existing session, creating a new branch with shared history. */
|
|
11944
12528
|
async forkSession(sessionId, cwd, mcpServers) {
|
|
11945
12529
|
const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
|
|
11946
12530
|
return await this.connection.unstable_forkSession({
|
|
@@ -11949,10 +12533,25 @@ ${stderr}`
|
|
|
11949
12533
|
mcpServers: resolvedMcp
|
|
11950
12534
|
});
|
|
11951
12535
|
}
|
|
12536
|
+
/** Close a session on the agent side (cleanup agent-internal state). */
|
|
11952
12537
|
async closeSession(sessionId) {
|
|
11953
12538
|
await this.connection.unstable_closeSession({ sessionId });
|
|
11954
12539
|
}
|
|
11955
12540
|
// ── Prompt & lifecycle ──────────────────────────────────────────────
|
|
12541
|
+
/**
|
|
12542
|
+
* Send a user prompt to the agent and wait for the complete response.
|
|
12543
|
+
*
|
|
12544
|
+
* Builds ACP content blocks from the text and any attachments (images
|
|
12545
|
+
* are base64-encoded if the agent supports them, otherwise appended as
|
|
12546
|
+
* file paths). The promise resolves when the agent finishes responding;
|
|
12547
|
+
* streaming events arrive via the `agent_event` emitter during execution.
|
|
12548
|
+
*
|
|
12549
|
+
* Attachments that exceed size limits or use unsupported formats are
|
|
12550
|
+
* skipped with a note appended to the prompt text.
|
|
12551
|
+
*
|
|
12552
|
+
* Call `cancel()` to interrupt a running prompt; the agent will stop and
|
|
12553
|
+
* the promise resolves with partial results.
|
|
12554
|
+
*/
|
|
11956
12555
|
async prompt(text3, attachments) {
|
|
11957
12556
|
const contentBlocks = [{ type: "text", text: text3 }];
|
|
11958
12557
|
const capabilities = this.promptCapabilities ?? {};
|
|
@@ -12002,9 +12601,18 @@ ${skipNote}`;
|
|
|
12002
12601
|
prompt: contentBlocks
|
|
12003
12602
|
});
|
|
12004
12603
|
}
|
|
12604
|
+
/** Cancel the currently running prompt. The agent should stop and return partial results. */
|
|
12005
12605
|
async cancel() {
|
|
12006
12606
|
await this.connection.cancel({ sessionId: this.sessionId });
|
|
12007
12607
|
}
|
|
12608
|
+
/**
|
|
12609
|
+
* Gracefully shut down the agent subprocess.
|
|
12610
|
+
*
|
|
12611
|
+
* Sends SIGTERM first, giving the agent up to 10 seconds to clean up.
|
|
12612
|
+
* If the process hasn't exited by then, SIGKILL forces termination.
|
|
12613
|
+
* The timer is unref'd so it doesn't keep the Node process alive
|
|
12614
|
+
* during shutdown.
|
|
12615
|
+
*/
|
|
12008
12616
|
async destroy() {
|
|
12009
12617
|
this._destroying = true;
|
|
12010
12618
|
this.terminalManager.destroyAll();
|
|
@@ -12051,6 +12659,7 @@ var AgentManager = class {
|
|
|
12051
12659
|
constructor(catalog) {
|
|
12052
12660
|
this.catalog = catalog;
|
|
12053
12661
|
}
|
|
12662
|
+
/** Return definitions for all installed agents. */
|
|
12054
12663
|
getAvailableAgents() {
|
|
12055
12664
|
const installed = this.catalog.getInstalledEntries();
|
|
12056
12665
|
return Object.entries(installed).map(([key, agent]) => ({
|
|
@@ -12060,14 +12669,25 @@ var AgentManager = class {
|
|
|
12060
12669
|
env: agent.env
|
|
12061
12670
|
}));
|
|
12062
12671
|
}
|
|
12672
|
+
/** Look up a single agent definition by its short name (e.g., "claude", "gemini"). */
|
|
12063
12673
|
getAgent(name) {
|
|
12064
12674
|
return this.catalog.resolve(name);
|
|
12065
12675
|
}
|
|
12676
|
+
/**
|
|
12677
|
+
* Spawn a new agent subprocess with a fresh session.
|
|
12678
|
+
*
|
|
12679
|
+
* @throws If the agent is not installed — includes install instructions in the error message.
|
|
12680
|
+
*/
|
|
12066
12681
|
async spawn(agentName, workingDirectory, allowedPaths) {
|
|
12067
12682
|
const agentDef = this.getAgent(agentName);
|
|
12068
12683
|
if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
|
|
12069
12684
|
return AgentInstance.spawn(agentDef, workingDirectory, void 0, allowedPaths);
|
|
12070
12685
|
}
|
|
12686
|
+
/**
|
|
12687
|
+
* Spawn a subprocess and resume an existing agent session.
|
|
12688
|
+
*
|
|
12689
|
+
* Falls back to a new session if the agent cannot restore the given session ID.
|
|
12690
|
+
*/
|
|
12071
12691
|
async resume(agentName, workingDirectory, agentSessionId, allowedPaths) {
|
|
12072
12692
|
const agentDef = this.getAgent(agentName);
|
|
12073
12693
|
if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
|
|
@@ -12089,6 +12709,11 @@ var PromptQueue = class {
|
|
|
12089
12709
|
abortController = null;
|
|
12090
12710
|
/** Set when abort is triggered; drainNext waits for the current processor to settle before starting the next item. */
|
|
12091
12711
|
processorSettled = null;
|
|
12712
|
+
/**
|
|
12713
|
+
* Add a prompt to the queue. If no prompt is currently processing, it runs
|
|
12714
|
+
* immediately. Otherwise, it's buffered and the returned promise resolves
|
|
12715
|
+
* only after the prompt finishes processing.
|
|
12716
|
+
*/
|
|
12092
12717
|
async enqueue(text3, attachments, routing, turnId) {
|
|
12093
12718
|
if (this.processing) {
|
|
12094
12719
|
return new Promise((resolve7) => {
|
|
@@ -12097,6 +12722,7 @@ var PromptQueue = class {
|
|
|
12097
12722
|
}
|
|
12098
12723
|
await this.process(text3, attachments, routing, turnId);
|
|
12099
12724
|
}
|
|
12725
|
+
/** Run a single prompt through the processor, then drain the next queued item. */
|
|
12100
12726
|
async process(text3, attachments, routing, turnId) {
|
|
12101
12727
|
this.processing = true;
|
|
12102
12728
|
this.abortController = new AbortController();
|
|
@@ -12124,12 +12750,17 @@ var PromptQueue = class {
|
|
|
12124
12750
|
this.drainNext();
|
|
12125
12751
|
}
|
|
12126
12752
|
}
|
|
12753
|
+
/** Dequeue and process the next pending prompt, if any. Called after each prompt completes. */
|
|
12127
12754
|
drainNext() {
|
|
12128
12755
|
const next = this.queue.shift();
|
|
12129
12756
|
if (next) {
|
|
12130
12757
|
this.process(next.text, next.attachments, next.routing, next.turnId).then(next.resolve);
|
|
12131
12758
|
}
|
|
12132
12759
|
}
|
|
12760
|
+
/**
|
|
12761
|
+
* Abort the in-flight prompt and discard all queued prompts.
|
|
12762
|
+
* Pending promises are resolved (not rejected) so callers don't see unhandled rejections.
|
|
12763
|
+
*/
|
|
12133
12764
|
clear() {
|
|
12134
12765
|
if (this.abortController) {
|
|
12135
12766
|
this.abortController.abort();
|
|
@@ -12159,6 +12790,10 @@ var PermissionGate = class {
|
|
|
12159
12790
|
constructor(timeoutMs) {
|
|
12160
12791
|
this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
12161
12792
|
}
|
|
12793
|
+
/**
|
|
12794
|
+
* Register a new permission request and return a promise that resolves with the
|
|
12795
|
+
* chosen option ID when the user responds, or rejects on timeout / supersession.
|
|
12796
|
+
*/
|
|
12162
12797
|
setPending(request) {
|
|
12163
12798
|
if (!this.settled && this.rejectFn) {
|
|
12164
12799
|
this.rejectFn(new Error("Superseded by new permission request"));
|
|
@@ -12177,6 +12812,7 @@ var PermissionGate = class {
|
|
|
12177
12812
|
}
|
|
12178
12813
|
});
|
|
12179
12814
|
}
|
|
12815
|
+
/** Approve the pending request with the given option ID. No-op if already settled. */
|
|
12180
12816
|
resolve(optionId) {
|
|
12181
12817
|
if (this.settled || !this.resolveFn) return;
|
|
12182
12818
|
this.settled = true;
|
|
@@ -12184,6 +12820,7 @@ var PermissionGate = class {
|
|
|
12184
12820
|
this.resolveFn(optionId);
|
|
12185
12821
|
this.cleanup();
|
|
12186
12822
|
}
|
|
12823
|
+
/** Deny the pending request. No-op if already settled. */
|
|
12187
12824
|
reject(reason) {
|
|
12188
12825
|
if (this.settled || !this.rejectFn) return;
|
|
12189
12826
|
this.settled = true;
|
|
@@ -12277,6 +12914,8 @@ var Session = class extends TypedEmitter {
|
|
|
12277
12914
|
get agentInstance() {
|
|
12278
12915
|
return this._agentInstance;
|
|
12279
12916
|
}
|
|
12917
|
+
/** Setting agentInstance wires the agent→session event relay and commands buffer.
|
|
12918
|
+
* This happens both at construction and on agent switch (switchAgent). */
|
|
12280
12919
|
set agentInstance(agent) {
|
|
12281
12920
|
this._agentInstance = agent;
|
|
12282
12921
|
this.wireAgentRelay();
|
|
@@ -12379,7 +13018,7 @@ var Session = class extends TypedEmitter {
|
|
|
12379
13018
|
this.transition("finished");
|
|
12380
13019
|
this.emit(SessionEv.SESSION_END, reason ?? "completed");
|
|
12381
13020
|
}
|
|
12382
|
-
/** Transition to cancelled — from active
|
|
13021
|
+
/** Transition to cancelled — from active or error (terminal session cancel) */
|
|
12383
13022
|
markCancelled() {
|
|
12384
13023
|
this.transition("cancelled");
|
|
12385
13024
|
}
|
|
@@ -12399,19 +13038,29 @@ var Session = class extends TypedEmitter {
|
|
|
12399
13038
|
get queueDepth() {
|
|
12400
13039
|
return this.queue.pending;
|
|
12401
13040
|
}
|
|
13041
|
+
/** Whether a prompt is currently being processed by the agent */
|
|
12402
13042
|
get promptRunning() {
|
|
12403
13043
|
return this.queue.isProcessing;
|
|
12404
13044
|
}
|
|
12405
13045
|
// --- Context Injection ---
|
|
13046
|
+
/** Store context markdown to be prepended to the next prompt (used for session resume with history). */
|
|
12406
13047
|
setContext(markdown) {
|
|
12407
13048
|
this.pendingContext = markdown;
|
|
12408
13049
|
}
|
|
12409
13050
|
// --- Voice Mode ---
|
|
13051
|
+
/** Set TTS mode: "off" = disabled, "next" = one-shot (auto-resets after prompt), "on" = persistent. */
|
|
12410
13052
|
setVoiceMode(mode) {
|
|
12411
13053
|
this.voiceMode = mode;
|
|
12412
13054
|
this.log.info({ voiceMode: mode }, "TTS mode changed");
|
|
12413
13055
|
}
|
|
12414
13056
|
// --- Public API ---
|
|
13057
|
+
/**
|
|
13058
|
+
* Enqueue a user prompt for serial processing.
|
|
13059
|
+
*
|
|
13060
|
+
* Runs the prompt through agent:beforePrompt middleware (which can modify or block),
|
|
13061
|
+
* then adds it to the PromptQueue. Returns a turnId that callers can use to correlate
|
|
13062
|
+
* queued/processing events before the prompt actually runs.
|
|
13063
|
+
*/
|
|
12415
13064
|
async enqueuePrompt(text3, attachments, routing, externalTurnId) {
|
|
12416
13065
|
const turnId = externalTurnId ?? nanoid2(8);
|
|
12417
13066
|
if (this.middlewareChain) {
|
|
@@ -12521,6 +13170,10 @@ ${text3}`;
|
|
|
12521
13170
|
await this.autoName();
|
|
12522
13171
|
}
|
|
12523
13172
|
}
|
|
13173
|
+
/**
|
|
13174
|
+
* Transcribe audio attachments to text if the agent doesn't support audio natively.
|
|
13175
|
+
* Audio attachments are removed and their transcriptions are appended to the prompt text.
|
|
13176
|
+
*/
|
|
12524
13177
|
async maybeTranscribeAudio(text3, attachments) {
|
|
12525
13178
|
if (!attachments?.length || !this.speechService) {
|
|
12526
13179
|
return { text: text3, attachments };
|
|
@@ -12566,6 +13219,7 @@ ${result.text}` : result.text;
|
|
|
12566
13219
|
attachments: remainingAttachments.length > 0 ? remainingAttachments : void 0
|
|
12567
13220
|
};
|
|
12568
13221
|
}
|
|
13222
|
+
/** Extract [TTS] block from agent response, synthesize speech, and emit audio_content event. */
|
|
12569
13223
|
async processTTSResponse(responseText) {
|
|
12570
13224
|
const match = TTS_BLOCK_REGEX.exec(responseText);
|
|
12571
13225
|
if (!match?.[1]) {
|
|
@@ -12602,7 +13256,9 @@ ${result.text}` : result.text;
|
|
|
12602
13256
|
this.log.warn({ err }, "TTS synthesis failed, skipping");
|
|
12603
13257
|
}
|
|
12604
13258
|
}
|
|
12605
|
-
//
|
|
13259
|
+
// Sends a special prompt to the agent to generate a short session title.
|
|
13260
|
+
// The session emitter is paused (excluding non-agent_event emissions) so the naming
|
|
13261
|
+
// prompt's output is intercepted by a capture handler instead of being forwarded to adapters.
|
|
12606
13262
|
async autoName() {
|
|
12607
13263
|
let title = "";
|
|
12608
13264
|
const captureHandler = (event) => {
|
|
@@ -12761,6 +13417,7 @@ ${result.text}` : result.text;
|
|
|
12761
13417
|
this.applySpawnResponse(newAgent.initialSessionResponse, newAgent.agentCapabilities);
|
|
12762
13418
|
this.log.info({ from: this.agentSwitchHistory.at(-1).agentName, to: agentName }, "Agent switched");
|
|
12763
13419
|
}
|
|
13420
|
+
/** Tear down the session: reject pending permissions, clear queue, destroy agent subprocess. */
|
|
12764
13421
|
async destroy() {
|
|
12765
13422
|
this.log.info("Session destroyed");
|
|
12766
13423
|
if (this.permissionGate.isPending) {
|
|
@@ -13055,6 +13712,12 @@ var MessageTransformer = class {
|
|
|
13055
13712
|
constructor(tunnelService) {
|
|
13056
13713
|
this.tunnelService = tunnelService;
|
|
13057
13714
|
}
|
|
13715
|
+
/**
|
|
13716
|
+
* Convert an agent event to an outgoing message for adapter delivery.
|
|
13717
|
+
*
|
|
13718
|
+
* For tool events, enriches the metadata with diff stats and viewer links
|
|
13719
|
+
* when a tunnel service is available.
|
|
13720
|
+
*/
|
|
13058
13721
|
transform(event, sessionContext) {
|
|
13059
13722
|
switch (event.type) {
|
|
13060
13723
|
case "text":
|
|
@@ -13285,12 +13948,17 @@ var SessionManager = class {
|
|
|
13285
13948
|
store;
|
|
13286
13949
|
eventBus;
|
|
13287
13950
|
middlewareChain;
|
|
13951
|
+
/**
|
|
13952
|
+
* Inject the EventBus after construction. Deferred because EventBus is created
|
|
13953
|
+
* after SessionManager during bootstrap, so it cannot be passed to the constructor.
|
|
13954
|
+
*/
|
|
13288
13955
|
setEventBus(eventBus) {
|
|
13289
13956
|
this.eventBus = eventBus;
|
|
13290
13957
|
}
|
|
13291
13958
|
constructor(store = null) {
|
|
13292
13959
|
this.store = store;
|
|
13293
13960
|
}
|
|
13961
|
+
/** Create a new session by spawning an agent and persisting the initial record. */
|
|
13294
13962
|
async createSession(channelId, agentName, workingDirectory, agentManager) {
|
|
13295
13963
|
const agentInstance = await agentManager.spawn(agentName, workingDirectory);
|
|
13296
13964
|
const session = new Session({
|
|
@@ -13318,9 +13986,11 @@ var SessionManager = class {
|
|
|
13318
13986
|
}
|
|
13319
13987
|
return session;
|
|
13320
13988
|
}
|
|
13989
|
+
/** Look up a live session by its OpenACP session ID. */
|
|
13321
13990
|
getSession(sessionId) {
|
|
13322
13991
|
return this.sessions.get(sessionId);
|
|
13323
13992
|
}
|
|
13993
|
+
/** Look up a live session by adapter channel and thread ID (checks per-adapter threadIds map first, then legacy fields). */
|
|
13324
13994
|
getSessionByThread(channelId, threadId) {
|
|
13325
13995
|
for (const session of this.sessions.values()) {
|
|
13326
13996
|
const adapterThread = session.threadIds.get(channelId);
|
|
@@ -13331,6 +14001,7 @@ var SessionManager = class {
|
|
|
13331
14001
|
}
|
|
13332
14002
|
return void 0;
|
|
13333
14003
|
}
|
|
14004
|
+
/** Look up a live session by the agent's internal session ID (assigned by the ACP subprocess). */
|
|
13334
14005
|
getSessionByAgentSessionId(agentSessionId) {
|
|
13335
14006
|
for (const session of this.sessions.values()) {
|
|
13336
14007
|
if (session.agentSessionId === agentSessionId) {
|
|
@@ -13339,18 +14010,26 @@ var SessionManager = class {
|
|
|
13339
14010
|
}
|
|
13340
14011
|
return void 0;
|
|
13341
14012
|
}
|
|
14013
|
+
/** Look up the persisted SessionRecord by the agent's internal session ID. */
|
|
13342
14014
|
getRecordByAgentSessionId(agentSessionId) {
|
|
13343
14015
|
return this.store?.findByAgentSessionId(agentSessionId);
|
|
13344
14016
|
}
|
|
14017
|
+
/** Look up the persisted SessionRecord by channel and thread ID. */
|
|
13345
14018
|
getRecordByThread(channelId, threadId) {
|
|
13346
14019
|
return this.store?.findByPlatform(
|
|
13347
14020
|
channelId,
|
|
13348
14021
|
(p) => String(p.topicId) === threadId || p.threadId === threadId
|
|
13349
14022
|
);
|
|
13350
14023
|
}
|
|
14024
|
+
/** Register a session that was created externally (e.g. restored from store on startup). */
|
|
13351
14025
|
registerSession(session) {
|
|
13352
14026
|
this.sessions.set(session.id, session);
|
|
13353
14027
|
}
|
|
14028
|
+
/**
|
|
14029
|
+
* Merge a partial update into the stored SessionRecord. If no record exists yet and
|
|
14030
|
+
* the patch includes `sessionId`, it is treated as an initial save.
|
|
14031
|
+
* Pass `{ immediate: true }` to flush the store to disk synchronously.
|
|
14032
|
+
*/
|
|
13354
14033
|
async patchRecord(sessionId, patch, options) {
|
|
13355
14034
|
if (!this.store) return;
|
|
13356
14035
|
const record = this.store.get(sessionId);
|
|
@@ -13363,9 +14042,11 @@ var SessionManager = class {
|
|
|
13363
14042
|
this.store.flush();
|
|
13364
14043
|
}
|
|
13365
14044
|
}
|
|
14045
|
+
/** Retrieve the persisted SessionRecord for a given session ID. Returns undefined if no store or record not found. */
|
|
13366
14046
|
getSessionRecord(sessionId) {
|
|
13367
14047
|
return this.store?.get(sessionId);
|
|
13368
14048
|
}
|
|
14049
|
+
/** Cancel a session: abort in-flight prompt, transition to cancelled, destroy agent, and persist. */
|
|
13369
14050
|
async cancelSession(sessionId) {
|
|
13370
14051
|
const session = this.sessions.get(sessionId);
|
|
13371
14052
|
if (session) {
|
|
@@ -13388,11 +14069,16 @@ var SessionManager = class {
|
|
|
13388
14069
|
});
|
|
13389
14070
|
}
|
|
13390
14071
|
}
|
|
14072
|
+
/** List live (in-memory) sessions, optionally filtered by channel. Excludes assistant sessions. */
|
|
13391
14073
|
listSessions(channelId) {
|
|
13392
14074
|
const all = Array.from(this.sessions.values()).filter((s) => !s.isAssistant);
|
|
13393
14075
|
if (channelId) return all.filter((s) => s.channelId === channelId);
|
|
13394
14076
|
return all;
|
|
13395
14077
|
}
|
|
14078
|
+
/**
|
|
14079
|
+
* List all sessions (live + stored) as SessionSummary. Live sessions take precedence
|
|
14080
|
+
* over stored records — their real-time state (queueDepth, promptRunning) is used.
|
|
14081
|
+
*/
|
|
13396
14082
|
listAllSessions(channelId) {
|
|
13397
14083
|
if (this.store) {
|
|
13398
14084
|
let records = this.store.list().filter((r) => !r.isAssistant);
|
|
@@ -13454,6 +14140,7 @@ var SessionManager = class {
|
|
|
13454
14140
|
isLive: true
|
|
13455
14141
|
}));
|
|
13456
14142
|
}
|
|
14143
|
+
/** List all stored SessionRecords, optionally filtered by status. Excludes assistant sessions. */
|
|
13457
14144
|
listRecords(filter) {
|
|
13458
14145
|
if (!this.store) return [];
|
|
13459
14146
|
let records = this.store.list().filter((r) => !r.isAssistant);
|
|
@@ -13462,6 +14149,7 @@ var SessionManager = class {
|
|
|
13462
14149
|
}
|
|
13463
14150
|
return records;
|
|
13464
14151
|
}
|
|
14152
|
+
/** Remove a session's stored record and emit a SESSION_DELETED event. */
|
|
13465
14153
|
async removeRecord(sessionId) {
|
|
13466
14154
|
if (!this.store) return;
|
|
13467
14155
|
await this.store.remove(sessionId);
|
|
@@ -13565,7 +14253,10 @@ var SessionBridge = class {
|
|
|
13565
14253
|
log6.error({ err, sessionId }, "Error in sendMessage middleware");
|
|
13566
14254
|
}
|
|
13567
14255
|
}
|
|
13568
|
-
/**
|
|
14256
|
+
/**
|
|
14257
|
+
* Determine if this bridge should forward the given event based on turn routing.
|
|
14258
|
+
* System events are always forwarded; turn events are routed only to the target adapter.
|
|
14259
|
+
*/
|
|
13569
14260
|
shouldForward(event) {
|
|
13570
14261
|
if (isSystemEvent(event)) return true;
|
|
13571
14262
|
const ctx = this.session.activeTurnContext;
|
|
@@ -13574,6 +14265,13 @@ var SessionBridge = class {
|
|
|
13574
14265
|
if (target === null) return false;
|
|
13575
14266
|
return this.adapterId === target;
|
|
13576
14267
|
}
|
|
14268
|
+
/**
|
|
14269
|
+
* Subscribe to session events and start forwarding them to the adapter.
|
|
14270
|
+
*
|
|
14271
|
+
* Wires: agent events → adapter dispatch, permission UI, lifecycle persistence
|
|
14272
|
+
* (status changes, naming, prompt count), and EventBus notifications.
|
|
14273
|
+
* Also replays any commands or config options that arrived before the bridge connected.
|
|
14274
|
+
*/
|
|
13577
14275
|
connect() {
|
|
13578
14276
|
if (this.connected) return;
|
|
13579
14277
|
this.connected = true;
|
|
@@ -13650,6 +14348,7 @@ var SessionBridge = class {
|
|
|
13650
14348
|
this.session.emit(SessionEv.AGENT_EVENT, { type: "config_option_update", options: this.session.configOptions });
|
|
13651
14349
|
}
|
|
13652
14350
|
}
|
|
14351
|
+
/** Unsubscribe all session event listeners and clean up adapter state. */
|
|
13653
14352
|
disconnect() {
|
|
13654
14353
|
if (!this.connected) return;
|
|
13655
14354
|
this.connected = false;
|
|
@@ -13922,6 +14621,10 @@ var SessionFactory = class {
|
|
|
13922
14621
|
get speechService() {
|
|
13923
14622
|
return typeof this.speechServiceAccessor === "function" ? this.speechServiceAccessor() : this.speechServiceAccessor;
|
|
13924
14623
|
}
|
|
14624
|
+
/**
|
|
14625
|
+
* Create a new Session: spawn agent → create Session instance → hydrate ACP state → register.
|
|
14626
|
+
* Runs session:beforeCreate middleware (which can modify params or block creation).
|
|
14627
|
+
*/
|
|
13925
14628
|
async create(params) {
|
|
13926
14629
|
let createParams = params;
|
|
13927
14630
|
if (this.middlewareChain) {
|
|
@@ -14105,6 +14808,11 @@ var SessionFactory = class {
|
|
|
14105
14808
|
this.resumeLocks.set(sessionId, resumePromise);
|
|
14106
14809
|
return resumePromise;
|
|
14107
14810
|
}
|
|
14811
|
+
/**
|
|
14812
|
+
* Attempt to resume a session from disk when a message arrives on a thread with
|
|
14813
|
+
* no live session. Deduplicates concurrent resume attempts for the same thread
|
|
14814
|
+
* via resumeLocks to avoid spawning multiple agents.
|
|
14815
|
+
*/
|
|
14108
14816
|
async lazyResume(channelId, threadId) {
|
|
14109
14817
|
const store = this.sessionStore;
|
|
14110
14818
|
if (!store || !this.createFullSession) return null;
|
|
@@ -14208,6 +14916,7 @@ var SessionFactory = class {
|
|
|
14208
14916
|
this.resumeLocks.set(lockKey, resumePromise);
|
|
14209
14917
|
return resumePromise;
|
|
14210
14918
|
}
|
|
14919
|
+
/** Create a brand-new session, resolving agent name and workspace from config if not provided. */
|
|
14211
14920
|
async handleNewSession(channelId, agentName, workspacePath, options) {
|
|
14212
14921
|
if (!this.configManager || !this.agentCatalog || !this.createFullSession) {
|
|
14213
14922
|
throw new Error("SessionFactory not fully initialized");
|
|
@@ -14252,6 +14961,7 @@ var SessionFactory = class {
|
|
|
14252
14961
|
record.workingDir
|
|
14253
14962
|
);
|
|
14254
14963
|
}
|
|
14964
|
+
/** Create a session and inject conversation context from a ContextProvider (e.g., history from a previous session). */
|
|
14255
14965
|
async createSessionWithContext(params) {
|
|
14256
14966
|
if (!this.createFullSession) throw new Error("SessionFactory not fully initialized");
|
|
14257
14967
|
let contextResult = null;
|
|
@@ -14278,6 +14988,7 @@ var SessionFactory = class {
|
|
|
14278
14988
|
}
|
|
14279
14989
|
return { session, contextResult };
|
|
14280
14990
|
}
|
|
14991
|
+
/** Wire session-level side effects: usage tracking (via EventBus) and tunnel cleanup on session end. */
|
|
14281
14992
|
wireSideEffects(session, deps) {
|
|
14282
14993
|
session.on(SessionEv.AGENT_EVENT, (event) => {
|
|
14283
14994
|
if (event.type !== "usage") return;
|
|
@@ -14358,6 +15069,11 @@ var JsonFileSessionStore = class {
|
|
|
14358
15069
|
}
|
|
14359
15070
|
return void 0;
|
|
14360
15071
|
}
|
|
15072
|
+
/**
|
|
15073
|
+
* Find a session by its ACP agent session ID.
|
|
15074
|
+
* Checks current, original, and historical agent session IDs (from agent switches)
|
|
15075
|
+
* since the agent session ID changes on each switch.
|
|
15076
|
+
*/
|
|
14361
15077
|
findByAgentSessionId(agentSessionId) {
|
|
14362
15078
|
for (const record of this.records.values()) {
|
|
14363
15079
|
if (record.agentSessionId === agentSessionId || record.originalAgentSessionId === agentSessionId) {
|
|
@@ -14402,6 +15118,7 @@ var JsonFileSessionStore = class {
|
|
|
14402
15118
|
if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
|
|
14403
15119
|
fs9.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
14404
15120
|
}
|
|
15121
|
+
/** Clean up timers and process listeners. Call on shutdown to prevent leaks. */
|
|
14405
15122
|
destroy() {
|
|
14406
15123
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
14407
15124
|
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
|
|
@@ -14437,7 +15154,11 @@ var JsonFileSessionStore = class {
|
|
|
14437
15154
|
}
|
|
14438
15155
|
}
|
|
14439
15156
|
}
|
|
14440
|
-
/**
|
|
15157
|
+
/**
|
|
15158
|
+
* Migrate old SessionRecord format to new multi-adapter format.
|
|
15159
|
+
* Converts single-adapter `platform` field to per-adapter `platforms` map,
|
|
15160
|
+
* and initializes `attachedAdapters` for records created before multi-adapter support.
|
|
15161
|
+
*/
|
|
14441
15162
|
migrateRecord(record) {
|
|
14442
15163
|
if (!record.platforms && record.platform && typeof record.platform === "object") {
|
|
14443
15164
|
const platformData = record.platform;
|
|
@@ -14450,6 +15171,7 @@ var JsonFileSessionStore = class {
|
|
|
14450
15171
|
}
|
|
14451
15172
|
return record;
|
|
14452
15173
|
}
|
|
15174
|
+
/** Remove expired session records (past TTL). Active and assistant sessions are preserved. */
|
|
14453
15175
|
cleanup() {
|
|
14454
15176
|
const cutoff = Date.now() - this.ttlDays * 24 * 60 * 60 * 1e3;
|
|
14455
15177
|
let removed = 0;
|
|
@@ -14492,7 +15214,12 @@ var AgentSwitchHandler = class {
|
|
|
14492
15214
|
constructor(deps) {
|
|
14493
15215
|
this.deps = deps;
|
|
14494
15216
|
}
|
|
15217
|
+
/** Prevents concurrent switch operations on the same session */
|
|
14495
15218
|
switchingLocks = /* @__PURE__ */ new Set();
|
|
15219
|
+
/**
|
|
15220
|
+
* Switch a session to a different agent. Returns whether the previous
|
|
15221
|
+
* agent session was resumed or a new one was spawned.
|
|
15222
|
+
*/
|
|
14496
15223
|
async switch(sessionId, toAgent) {
|
|
14497
15224
|
if (this.switchingLocks.has(sessionId)) {
|
|
14498
15225
|
throw new Error("Switch already in progress");
|
|
@@ -14677,20 +15404,29 @@ var REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/regis
|
|
|
14677
15404
|
var DEFAULT_TTL_HOURS = 24;
|
|
14678
15405
|
var AgentCatalog = class {
|
|
14679
15406
|
store;
|
|
15407
|
+
/** Agents available in the remote registry (cached in memory after load). */
|
|
14680
15408
|
registryAgents = [];
|
|
14681
15409
|
cachePath;
|
|
15410
|
+
/** Directory where binary agent archives are extracted to. */
|
|
14682
15411
|
agentsDir;
|
|
14683
15412
|
constructor(store, cachePath, agentsDir) {
|
|
14684
15413
|
this.store = store;
|
|
14685
15414
|
this.cachePath = cachePath;
|
|
14686
15415
|
this.agentsDir = agentsDir;
|
|
14687
15416
|
}
|
|
15417
|
+
/**
|
|
15418
|
+
* Load installed agents from disk and hydrate the registry from cache/snapshot.
|
|
15419
|
+
*
|
|
15420
|
+
* Also enriches installed agents with registry metadata — fixes agents that
|
|
15421
|
+
* were migrated from older config formats with incomplete data.
|
|
15422
|
+
*/
|
|
14688
15423
|
load() {
|
|
14689
15424
|
this.store.load();
|
|
14690
15425
|
this.loadRegistryFromCacheOrSnapshot();
|
|
14691
15426
|
this.enrichInstalledFromRegistry();
|
|
14692
15427
|
}
|
|
14693
15428
|
// --- Registry ---
|
|
15429
|
+
/** Fetch the latest agent registry from the CDN and update the local cache. */
|
|
14694
15430
|
async fetchRegistry() {
|
|
14695
15431
|
try {
|
|
14696
15432
|
log11.info("Fetching agent registry from CDN...");
|
|
@@ -14710,6 +15446,7 @@ var AgentCatalog = class {
|
|
|
14710
15446
|
log11.warn({ err }, "Failed to fetch registry, using cached data");
|
|
14711
15447
|
}
|
|
14712
15448
|
}
|
|
15449
|
+
/** Re-fetch registry only if the local cache has expired (24-hour TTL). */
|
|
14713
15450
|
async refreshRegistryIfStale() {
|
|
14714
15451
|
if (this.isCacheStale()) {
|
|
14715
15452
|
await this.fetchRegistry();
|
|
@@ -14721,6 +15458,7 @@ var AgentCatalog = class {
|
|
|
14721
15458
|
getRegistryAgent(registryId) {
|
|
14722
15459
|
return this.registryAgents.find((a) => a.id === registryId);
|
|
14723
15460
|
}
|
|
15461
|
+
/** Find a registry agent by registry ID or by its short alias (e.g., "claude"). */
|
|
14724
15462
|
findRegistryAgent(keyOrId) {
|
|
14725
15463
|
const byId = this.registryAgents.find((a) => a.id === keyOrId);
|
|
14726
15464
|
if (byId) return byId;
|
|
@@ -14737,6 +15475,15 @@ var AgentCatalog = class {
|
|
|
14737
15475
|
return this.store.getAgent(key);
|
|
14738
15476
|
}
|
|
14739
15477
|
// --- Discovery ---
|
|
15478
|
+
/**
|
|
15479
|
+
* Build the unified list of all agents (installed + registry-only).
|
|
15480
|
+
*
|
|
15481
|
+
* Installed agents appear first with their live availability status.
|
|
15482
|
+
* Registry agents that aren't installed yet show whether a distribution
|
|
15483
|
+
* exists for the current platform. Missing external dependencies
|
|
15484
|
+
* (e.g., claude CLI) are surfaced as `missingDeps` for UI display
|
|
15485
|
+
* but do NOT block installation.
|
|
15486
|
+
*/
|
|
14740
15487
|
getAvailable() {
|
|
14741
15488
|
const installed = this.getInstalledEntries();
|
|
14742
15489
|
const items = [];
|
|
@@ -14777,6 +15524,7 @@ var AgentCatalog = class {
|
|
|
14777
15524
|
}
|
|
14778
15525
|
return items;
|
|
14779
15526
|
}
|
|
15527
|
+
/** Check if an agent can be installed on this system (platform + dependencies). */
|
|
14780
15528
|
checkAvailability(keyOrId) {
|
|
14781
15529
|
const agent = this.findRegistryAgent(keyOrId);
|
|
14782
15530
|
if (!agent) return { available: false, reason: "Not found in the agent registry." };
|
|
@@ -14787,6 +15535,12 @@ var AgentCatalog = class {
|
|
|
14787
15535
|
return checkDependencies(agent.id);
|
|
14788
15536
|
}
|
|
14789
15537
|
// --- Install/Uninstall ---
|
|
15538
|
+
/**
|
|
15539
|
+
* Install an agent from the registry.
|
|
15540
|
+
*
|
|
15541
|
+
* Resolves the distribution (npx/uvx/binary), downloads binary archives
|
|
15542
|
+
* if needed, and persists the agent definition in the store.
|
|
15543
|
+
*/
|
|
14790
15544
|
async install(keyOrId, progress, force) {
|
|
14791
15545
|
const agent = this.findRegistryAgent(keyOrId);
|
|
14792
15546
|
if (!agent) {
|
|
@@ -14811,6 +15565,7 @@ var AgentCatalog = class {
|
|
|
14811
15565
|
registerFallbackAgent(key, data) {
|
|
14812
15566
|
this.store.addAgent(key, data);
|
|
14813
15567
|
}
|
|
15568
|
+
/** Remove an installed agent and delete its binary directory if applicable. */
|
|
14814
15569
|
async uninstall(key) {
|
|
14815
15570
|
if (this.store.hasAgent(key)) {
|
|
14816
15571
|
await uninstallAgent(key, this.store);
|
|
@@ -14819,6 +15574,7 @@ var AgentCatalog = class {
|
|
|
14819
15574
|
return { ok: false, error: `"${key}" is not installed.` };
|
|
14820
15575
|
}
|
|
14821
15576
|
// --- Resolution (for AgentManager) ---
|
|
15577
|
+
/** Convert an installed agent's short key to an AgentDefinition for spawning. */
|
|
14822
15578
|
resolve(key) {
|
|
14823
15579
|
const agent = this.store.getAgent(key);
|
|
14824
15580
|
if (!agent) return void 0;
|
|
@@ -14968,6 +15724,7 @@ var AgentStore = class {
|
|
|
14968
15724
|
constructor(filePath) {
|
|
14969
15725
|
this.filePath = filePath;
|
|
14970
15726
|
}
|
|
15727
|
+
/** Load and validate the store from disk. Starts fresh if file is missing or invalid. */
|
|
14971
15728
|
load() {
|
|
14972
15729
|
if (!fs13.existsSync(this.filePath)) {
|
|
14973
15730
|
this.data = { version: 1, installed: {} };
|
|
@@ -15007,6 +15764,11 @@ var AgentStore = class {
|
|
|
15007
15764
|
hasAgent(key) {
|
|
15008
15765
|
return key in this.data.installed;
|
|
15009
15766
|
}
|
|
15767
|
+
/**
|
|
15768
|
+
* Persist the store to disk using atomic write (write to .tmp, then rename).
|
|
15769
|
+
* File permissions are restricted to owner-only (0o600) since the store
|
|
15770
|
+
* may contain agent binary paths and environment variables.
|
|
15771
|
+
*/
|
|
15010
15772
|
save() {
|
|
15011
15773
|
fs13.mkdirSync(path12.dirname(this.filePath), { recursive: true });
|
|
15012
15774
|
const tmpPath = this.filePath + ".tmp";
|
|
@@ -15086,6 +15848,10 @@ function resolveLoadOrder(plugins) {
|
|
|
15086
15848
|
// src/core/plugin/service-registry.ts
|
|
15087
15849
|
var ServiceRegistry = class {
|
|
15088
15850
|
services = /* @__PURE__ */ new Map();
|
|
15851
|
+
/**
|
|
15852
|
+
* Register a service. Throws if the service name is already taken.
|
|
15853
|
+
* Use `registerOverride` to intentionally replace an existing service.
|
|
15854
|
+
*/
|
|
15089
15855
|
register(name, implementation, pluginName) {
|
|
15090
15856
|
if (this.services.has(name)) {
|
|
15091
15857
|
const existing = this.services.get(name);
|
|
@@ -15093,21 +15859,27 @@ var ServiceRegistry = class {
|
|
|
15093
15859
|
}
|
|
15094
15860
|
this.services.set(name, { implementation, pluginName });
|
|
15095
15861
|
}
|
|
15862
|
+
/** Register a service, replacing any existing registration (used by override plugins). */
|
|
15096
15863
|
registerOverride(name, implementation, pluginName) {
|
|
15097
15864
|
this.services.set(name, { implementation, pluginName });
|
|
15098
15865
|
}
|
|
15866
|
+
/** Retrieve a service by name. Returns undefined if not registered. */
|
|
15099
15867
|
get(name) {
|
|
15100
15868
|
return this.services.get(name)?.implementation;
|
|
15101
15869
|
}
|
|
15870
|
+
/** Check whether a service is registered. */
|
|
15102
15871
|
has(name) {
|
|
15103
15872
|
return this.services.has(name);
|
|
15104
15873
|
}
|
|
15874
|
+
/** List all registered services with their owning plugin names. */
|
|
15105
15875
|
list() {
|
|
15106
15876
|
return [...this.services.entries()].map(([name, { pluginName }]) => ({ name, pluginName }));
|
|
15107
15877
|
}
|
|
15878
|
+
/** Remove a single service by name. */
|
|
15108
15879
|
unregister(name) {
|
|
15109
15880
|
this.services.delete(name);
|
|
15110
15881
|
}
|
|
15882
|
+
/** Remove all services owned by a specific plugin (called during plugin unload). */
|
|
15111
15883
|
unregisterByPlugin(pluginName) {
|
|
15112
15884
|
for (const [name, entry] of this.services) {
|
|
15113
15885
|
if (entry.pluginName === pluginName) {
|
|
@@ -15123,6 +15895,7 @@ var MiddlewareChain = class {
|
|
|
15123
15895
|
chains = /* @__PURE__ */ new Map();
|
|
15124
15896
|
errorHandler;
|
|
15125
15897
|
errorTracker;
|
|
15898
|
+
/** Register a middleware handler for a hook. Handlers are kept sorted by priority. */
|
|
15126
15899
|
add(hook, pluginName, opts) {
|
|
15127
15900
|
const entry = {
|
|
15128
15901
|
pluginName,
|
|
@@ -15137,6 +15910,15 @@ var MiddlewareChain = class {
|
|
|
15137
15910
|
this.chains.set(hook, [entry]);
|
|
15138
15911
|
}
|
|
15139
15912
|
}
|
|
15913
|
+
/**
|
|
15914
|
+
* Execute the middleware chain for a hook, ending with the core handler.
|
|
15915
|
+
*
|
|
15916
|
+
* The chain is built recursively: each handler calls `next()` to invoke the
|
|
15917
|
+
* next handler, with the core handler at the end. If no middleware is registered,
|
|
15918
|
+
* the core handler runs directly.
|
|
15919
|
+
*
|
|
15920
|
+
* @returns The final payload, or `null` if any handler short-circuited.
|
|
15921
|
+
*/
|
|
15140
15922
|
async execute(hook, payload, coreHandler) {
|
|
15141
15923
|
const handlers = this.chains.get(hook);
|
|
15142
15924
|
if (!handlers || handlers.length === 0) {
|
|
@@ -15206,6 +15988,7 @@ var MiddlewareChain = class {
|
|
|
15206
15988
|
const start = buildNext(0, payload);
|
|
15207
15989
|
return start();
|
|
15208
15990
|
}
|
|
15991
|
+
/** Remove all middleware handlers registered by a specific plugin. */
|
|
15209
15992
|
removeAll(pluginName) {
|
|
15210
15993
|
for (const [hook, handlers] of this.chains.entries()) {
|
|
15211
15994
|
const filtered = handlers.filter((h) => h.pluginName !== pluginName);
|
|
@@ -15216,9 +15999,11 @@ var MiddlewareChain = class {
|
|
|
15216
15999
|
}
|
|
15217
16000
|
}
|
|
15218
16001
|
}
|
|
16002
|
+
/** Set a callback for middleware errors (e.g., logging). */
|
|
15219
16003
|
setErrorHandler(fn) {
|
|
15220
16004
|
this.errorHandler = fn;
|
|
15221
16005
|
}
|
|
16006
|
+
/** Attach an ErrorTracker for circuit-breaking misbehaving plugins. */
|
|
15222
16007
|
setErrorTracker(tracker) {
|
|
15223
16008
|
this.errorTracker = tracker;
|
|
15224
16009
|
}
|
|
@@ -15230,10 +16015,15 @@ var ErrorTracker = class {
|
|
|
15230
16015
|
disabled = /* @__PURE__ */ new Set();
|
|
15231
16016
|
exempt = /* @__PURE__ */ new Set();
|
|
15232
16017
|
config;
|
|
16018
|
+
/** Callback fired when a plugin is auto-disabled due to error budget exhaustion. */
|
|
15233
16019
|
onDisabled;
|
|
15234
16020
|
constructor(config) {
|
|
15235
16021
|
this.config = { maxErrors: config?.maxErrors ?? 10, windowMs: config?.windowMs ?? 36e5 };
|
|
15236
16022
|
}
|
|
16023
|
+
/**
|
|
16024
|
+
* Record an error for a plugin. If the error budget is exceeded,
|
|
16025
|
+
* the plugin is disabled and the `onDisabled` callback fires.
|
|
16026
|
+
*/
|
|
15237
16027
|
increment(pluginName) {
|
|
15238
16028
|
if (this.exempt.has(pluginName)) return;
|
|
15239
16029
|
const now = Date.now();
|
|
@@ -15250,13 +16040,16 @@ var ErrorTracker = class {
|
|
|
15250
16040
|
this.onDisabled?.(pluginName, reason);
|
|
15251
16041
|
}
|
|
15252
16042
|
}
|
|
16043
|
+
/** Check if a plugin has been disabled due to errors. */
|
|
15253
16044
|
isDisabled(pluginName) {
|
|
15254
16045
|
return this.disabled.has(pluginName);
|
|
15255
16046
|
}
|
|
16047
|
+
/** Re-enable a plugin and clear its error history. */
|
|
15256
16048
|
reset(pluginName) {
|
|
15257
16049
|
this.disabled.delete(pluginName);
|
|
15258
16050
|
this.errors.delete(pluginName);
|
|
15259
16051
|
}
|
|
16052
|
+
/** Mark a plugin as exempt from circuit-breaking (e.g., essential plugins). */
|
|
15260
16053
|
setExempt(pluginName) {
|
|
15261
16054
|
this.exempt.add(pluginName);
|
|
15262
16055
|
}
|
|
@@ -15268,6 +16061,7 @@ import path13 from "path";
|
|
|
15268
16061
|
var PluginStorageImpl = class {
|
|
15269
16062
|
kvPath;
|
|
15270
16063
|
dataDir;
|
|
16064
|
+
/** Serializes writes to prevent concurrent file corruption */
|
|
15271
16065
|
writeChain = Promise.resolve();
|
|
15272
16066
|
constructor(baseDir) {
|
|
15273
16067
|
this.dataDir = path13.join(baseDir, "data");
|
|
@@ -15308,6 +16102,7 @@ var PluginStorageImpl = class {
|
|
|
15308
16102
|
async list() {
|
|
15309
16103
|
return Object.keys(this.readKv());
|
|
15310
16104
|
}
|
|
16105
|
+
/** Returns the plugin's data directory, creating it lazily on first access. */
|
|
15311
16106
|
getDataDir() {
|
|
15312
16107
|
fs14.mkdirSync(this.dataDir, { recursive: true });
|
|
15313
16108
|
return this.dataDir;
|
|
@@ -15481,6 +16276,11 @@ function createPluginContext(opts) {
|
|
|
15481
16276
|
return core;
|
|
15482
16277
|
},
|
|
15483
16278
|
instanceRoot,
|
|
16279
|
+
/**
|
|
16280
|
+
* Called by LifecycleManager during plugin teardown.
|
|
16281
|
+
* Unregisters all event handlers, middleware, commands, and services
|
|
16282
|
+
* registered by this plugin, preventing leaks across reloads.
|
|
16283
|
+
*/
|
|
15484
16284
|
cleanup() {
|
|
15485
16285
|
for (const { event, handler } of registeredListeners) {
|
|
15486
16286
|
eventBus.off(event, handler);
|
|
@@ -15572,12 +16372,15 @@ var LifecycleManager = class {
|
|
|
15572
16372
|
loadOrder = [];
|
|
15573
16373
|
_loaded = /* @__PURE__ */ new Set();
|
|
15574
16374
|
_failed = /* @__PURE__ */ new Set();
|
|
16375
|
+
/** Names of plugins that successfully completed setup(). */
|
|
15575
16376
|
get loadedPlugins() {
|
|
15576
16377
|
return [...this._loaded];
|
|
15577
16378
|
}
|
|
16379
|
+
/** Names of plugins whose setup() threw an error. These plugins are skipped but don't crash the system. */
|
|
15578
16380
|
get failedPlugins() {
|
|
15579
16381
|
return [...this._failed];
|
|
15580
16382
|
}
|
|
16383
|
+
/** The PluginRegistry tracking installed and enabled plugin state. */
|
|
15581
16384
|
get registry() {
|
|
15582
16385
|
return this.pluginRegistry;
|
|
15583
16386
|
}
|
|
@@ -15624,6 +16427,12 @@ var LifecycleManager = class {
|
|
|
15624
16427
|
return this;
|
|
15625
16428
|
} };
|
|
15626
16429
|
}
|
|
16430
|
+
/**
|
|
16431
|
+
* Boot a set of plugins in dependency order.
|
|
16432
|
+
*
|
|
16433
|
+
* Can be called multiple times (e.g., core plugins first, then dev plugins later).
|
|
16434
|
+
* Already-loaded plugins are included in dependency resolution but not re-booted.
|
|
16435
|
+
*/
|
|
15627
16436
|
async boot(plugins) {
|
|
15628
16437
|
const newNames = new Set(plugins.map((p) => p.name));
|
|
15629
16438
|
const allForResolution = [...this.loadOrder.filter((p) => !newNames.has(p.name)), ...plugins];
|
|
@@ -15735,6 +16544,11 @@ var LifecycleManager = class {
|
|
|
15735
16544
|
}
|
|
15736
16545
|
}
|
|
15737
16546
|
}
|
|
16547
|
+
/**
|
|
16548
|
+
* Unload a single plugin: call teardown(), clean up its context
|
|
16549
|
+
* (listeners, middleware, services), and remove from tracked state.
|
|
16550
|
+
* Used for hot-reload: unload → rebuild → re-boot.
|
|
16551
|
+
*/
|
|
15738
16552
|
async unloadPlugin(name) {
|
|
15739
16553
|
if (!this._loaded.has(name)) return;
|
|
15740
16554
|
const plugin = this.loadOrder.find((p) => p.name === name);
|
|
@@ -15754,6 +16568,10 @@ var LifecycleManager = class {
|
|
|
15754
16568
|
this.loadOrder = this.loadOrder.filter((p) => p.name !== name);
|
|
15755
16569
|
this.eventBus?.emit(BusEvent.PLUGIN_UNLOADED, { name });
|
|
15756
16570
|
}
|
|
16571
|
+
/**
|
|
16572
|
+
* Gracefully shut down all loaded plugins.
|
|
16573
|
+
* Teardown runs in reverse boot order so that dependencies outlive their dependents.
|
|
16574
|
+
*/
|
|
15757
16575
|
async shutdown() {
|
|
15758
16576
|
const reversed = [...this.loadOrder].reverse();
|
|
15759
16577
|
for (const plugin of reversed) {
|
|
@@ -15781,16 +16599,19 @@ init_log();
|
|
|
15781
16599
|
var log13 = createChildLogger({ module: "menu-registry" });
|
|
15782
16600
|
var MenuRegistry = class {
|
|
15783
16601
|
items = /* @__PURE__ */ new Map();
|
|
16602
|
+
/** Register or replace a menu item by its unique ID. */
|
|
15784
16603
|
register(item) {
|
|
15785
16604
|
this.items.set(item.id, item);
|
|
15786
16605
|
}
|
|
16606
|
+
/** Remove a menu item by ID. */
|
|
15787
16607
|
unregister(id) {
|
|
15788
16608
|
this.items.delete(id);
|
|
15789
16609
|
}
|
|
16610
|
+
/** Look up a single menu item by ID. */
|
|
15790
16611
|
getItem(id) {
|
|
15791
16612
|
return this.items.get(id);
|
|
15792
16613
|
}
|
|
15793
|
-
/** Get all visible items sorted by priority */
|
|
16614
|
+
/** Get all visible items sorted by priority (lower number = shown first). */
|
|
15794
16615
|
getItems() {
|
|
15795
16616
|
return [...this.items.values()].filter((item) => {
|
|
15796
16617
|
if (!item.visible) return true;
|
|
@@ -15869,19 +16690,31 @@ var log14 = createChildLogger({ module: "assistant-registry" });
|
|
|
15869
16690
|
var AssistantRegistry = class {
|
|
15870
16691
|
sections = /* @__PURE__ */ new Map();
|
|
15871
16692
|
_instanceRoot = "";
|
|
15872
|
-
/** Set the instance root path used in assistant guidelines */
|
|
16693
|
+
/** Set the instance root path used in assistant guidelines. */
|
|
15873
16694
|
setInstanceRoot(root) {
|
|
15874
16695
|
this._instanceRoot = root;
|
|
15875
16696
|
}
|
|
16697
|
+
/** Register a prompt section. Overwrites any existing section with the same id. */
|
|
15876
16698
|
register(section) {
|
|
15877
16699
|
if (this.sections.has(section.id)) {
|
|
15878
16700
|
log14.warn({ id: section.id }, "Assistant section overwritten");
|
|
15879
16701
|
}
|
|
15880
16702
|
this.sections.set(section.id, section);
|
|
15881
16703
|
}
|
|
16704
|
+
/** Remove a previously registered section by id. */
|
|
15882
16705
|
unregister(id) {
|
|
15883
16706
|
this.sections.delete(id);
|
|
15884
16707
|
}
|
|
16708
|
+
/**
|
|
16709
|
+
* Compose the full system prompt from all registered sections.
|
|
16710
|
+
*
|
|
16711
|
+
* Sections are sorted by priority (ascending), each contributing a titled
|
|
16712
|
+
* markdown block. If a section's `buildContext()` throws, it is skipped
|
|
16713
|
+
* gracefully so one broken section doesn't break the entire prompt.
|
|
16714
|
+
*
|
|
16715
|
+
* If `channelId` is provided, a "Current Channel" block is injected at the
|
|
16716
|
+
* top of the prompt so the assistant can adapt its behavior to the platform.
|
|
16717
|
+
*/
|
|
15885
16718
|
buildSystemPrompt(channelId) {
|
|
15886
16719
|
const sorted = [...this.sections.values()].sort((a, b) => a.priority - b.priority);
|
|
15887
16720
|
const parts = [ASSISTANT_PREAMBLE];
|
|
@@ -15918,6 +16751,13 @@ var AssistantManager = class {
|
|
|
15918
16751
|
}
|
|
15919
16752
|
sessions = /* @__PURE__ */ new Map();
|
|
15920
16753
|
pendingSystemPrompts = /* @__PURE__ */ new Map();
|
|
16754
|
+
/**
|
|
16755
|
+
* Returns the assistant session for a channel, creating one if needed.
|
|
16756
|
+
*
|
|
16757
|
+
* If a persisted assistant session exists in the store, it is reused
|
|
16758
|
+
* (same session ID) to preserve conversation history. The system prompt
|
|
16759
|
+
* is always rebuilt fresh and deferred until the first user message.
|
|
16760
|
+
*/
|
|
15921
16761
|
async getOrSpawn(channelId, threadId) {
|
|
15922
16762
|
const existing = this.core.sessionStore?.findAssistant(channelId);
|
|
15923
16763
|
const session = await this.core.createSession({
|
|
@@ -15938,6 +16778,7 @@ var AssistantManager = class {
|
|
|
15938
16778
|
);
|
|
15939
16779
|
return session;
|
|
15940
16780
|
}
|
|
16781
|
+
/** Returns the active assistant session for a channel, or null if none exists. */
|
|
15941
16782
|
get(channelId) {
|
|
15942
16783
|
return this.sessions.get(channelId) ?? null;
|
|
15943
16784
|
}
|
|
@@ -15950,6 +16791,7 @@ var AssistantManager = class {
|
|
|
15950
16791
|
if (prompt) this.pendingSystemPrompts.delete(channelId);
|
|
15951
16792
|
return prompt;
|
|
15952
16793
|
}
|
|
16794
|
+
/** Checks whether a given session ID belongs to the built-in assistant. */
|
|
15953
16795
|
isAssistant(sessionId) {
|
|
15954
16796
|
for (const s of this.sessions.values()) {
|
|
15955
16797
|
if (s.id === sessionId) return true;
|
|
@@ -16176,30 +17018,49 @@ var OpenACPCore = class {
|
|
|
16176
17018
|
menuRegistry = new MenuRegistry();
|
|
16177
17019
|
assistantRegistry = new AssistantRegistry();
|
|
16178
17020
|
assistantManager;
|
|
16179
|
-
//
|
|
17021
|
+
// Services (security, notifications, speech, etc.) are provided by plugins that
|
|
17022
|
+
// register during boot. Core accesses them lazily via ServiceRegistry so it doesn't
|
|
17023
|
+
// need compile-time dependencies on plugin implementations.
|
|
17024
|
+
/** @throws if the service hasn't been registered by its plugin yet */
|
|
16180
17025
|
getService(name) {
|
|
16181
17026
|
const svc = this.lifecycleManager.serviceRegistry.get(name);
|
|
16182
17027
|
if (!svc) throw new Error(`Service '${name}' not registered \u2014 is the ${name} plugin loaded?`);
|
|
16183
17028
|
return svc;
|
|
16184
17029
|
}
|
|
17030
|
+
/** Access control and rate-limiting guard (provided by security plugin). */
|
|
16185
17031
|
get securityGuard() {
|
|
16186
17032
|
return this.getService("security");
|
|
16187
17033
|
}
|
|
17034
|
+
/** Cross-session notification delivery (provided by notifications plugin). */
|
|
16188
17035
|
get notificationManager() {
|
|
16189
17036
|
return this.getService("notifications");
|
|
16190
17037
|
}
|
|
17038
|
+
/** File I/O service for agent attachment storage (provided by file-service plugin). */
|
|
16191
17039
|
get fileService() {
|
|
16192
17040
|
return this.getService("file-service");
|
|
16193
17041
|
}
|
|
17042
|
+
/** Text-to-speech / speech-to-text engine (provided by speech plugin). */
|
|
16194
17043
|
get speechService() {
|
|
16195
17044
|
return this.getService("speech");
|
|
16196
17045
|
}
|
|
17046
|
+
/** Conversation history builder for context injection (provided by context plugin). */
|
|
16197
17047
|
get contextManager() {
|
|
16198
17048
|
return this.getService("context");
|
|
16199
17049
|
}
|
|
17050
|
+
/** Per-plugin persistent settings (e.g. API keys). */
|
|
16200
17051
|
get settingsManager() {
|
|
16201
17052
|
return this.lifecycleManager.settingsManager;
|
|
16202
17053
|
}
|
|
17054
|
+
/**
|
|
17055
|
+
* Bootstrap all core subsystems. The boot order matters:
|
|
17056
|
+
* 1. AgentCatalog + AgentManager (agent definitions)
|
|
17057
|
+
* 2. SessionStore + SessionManager (session persistence and lookup)
|
|
17058
|
+
* 3. EventBus (inter-module communication)
|
|
17059
|
+
* 4. SessionFactory (session creation pipeline)
|
|
17060
|
+
* 5. LifecycleManager (plugin infrastructure)
|
|
17061
|
+
* 6. Wire middleware chain into factory + manager
|
|
17062
|
+
* 7. AgentSwitchHandler, config listeners, menu/assistant registries
|
|
17063
|
+
*/
|
|
16203
17064
|
constructor(configManager, ctx) {
|
|
16204
17065
|
this.configManager = configManager;
|
|
16205
17066
|
this.instanceContext = ctx;
|
|
@@ -16310,16 +17171,28 @@ var OpenACPCore = class {
|
|
|
16310
17171
|
this.lifecycleManager.serviceRegistry.register("menu-registry", this.menuRegistry, "core");
|
|
16311
17172
|
this.lifecycleManager.serviceRegistry.register("assistant-registry", this.assistantRegistry, "core");
|
|
16312
17173
|
}
|
|
17174
|
+
/** Optional tunnel for generating public URLs (code viewer links, etc.). */
|
|
16313
17175
|
get tunnelService() {
|
|
16314
17176
|
return this._tunnelService;
|
|
16315
17177
|
}
|
|
17178
|
+
/** Propagate tunnel service to MessageTransformer so it can generate viewer links. */
|
|
16316
17179
|
set tunnelService(service) {
|
|
16317
17180
|
this._tunnelService = service;
|
|
16318
17181
|
this.messageTransformer.tunnelService = service;
|
|
16319
17182
|
}
|
|
17183
|
+
/**
|
|
17184
|
+
* Register a messaging adapter (e.g. Telegram, Slack, SSE).
|
|
17185
|
+
*
|
|
17186
|
+
* Adapters must be registered before `start()`. The adapter name serves as its
|
|
17187
|
+
* channel ID throughout the system — used in session records, bridge keys, and routing.
|
|
17188
|
+
*/
|
|
16320
17189
|
registerAdapter(name, adapter) {
|
|
16321
17190
|
this.adapters.set(name, adapter);
|
|
16322
17191
|
}
|
|
17192
|
+
/**
|
|
17193
|
+
* Start all registered adapters. Adapters that fail are logged but do not
|
|
17194
|
+
* prevent others from starting. Throws only if ALL adapters fail.
|
|
17195
|
+
*/
|
|
16323
17196
|
async start() {
|
|
16324
17197
|
this.agentCatalog.refreshRegistryIfStale().catch((err) => {
|
|
16325
17198
|
log16.warn({ err }, "Background registry refresh failed");
|
|
@@ -16339,6 +17212,10 @@ var OpenACPCore = class {
|
|
|
16339
17212
|
);
|
|
16340
17213
|
}
|
|
16341
17214
|
}
|
|
17215
|
+
/**
|
|
17216
|
+
* Graceful shutdown: notify users, persist session state, stop adapters.
|
|
17217
|
+
* Agent subprocesses are not explicitly killed — they exit with the parent process.
|
|
17218
|
+
*/
|
|
16342
17219
|
async stop() {
|
|
16343
17220
|
try {
|
|
16344
17221
|
const nm = this.lifecycleManager.serviceRegistry.get("notifications");
|
|
@@ -16357,6 +17234,13 @@ var OpenACPCore = class {
|
|
|
16357
17234
|
}
|
|
16358
17235
|
}
|
|
16359
17236
|
// --- Archive ---
|
|
17237
|
+
/**
|
|
17238
|
+
* Archive a session: delete its adapter topic/thread and cancel the session.
|
|
17239
|
+
*
|
|
17240
|
+
* Only sessions in archivable states (active, cancelled, error) can be archived —
|
|
17241
|
+
* initializing and finished sessions are excluded.
|
|
17242
|
+
* The adapter handles platform-side cleanup (e.g. deleting a Telegram topic).
|
|
17243
|
+
*/
|
|
16360
17244
|
async archiveSession(sessionId) {
|
|
16361
17245
|
const session = this.sessionManager.getSession(sessionId);
|
|
16362
17246
|
if (!session) return { ok: false, error: "Session not found (must be in memory)" };
|
|
@@ -16376,6 +17260,18 @@ var OpenACPCore = class {
|
|
|
16376
17260
|
}
|
|
16377
17261
|
}
|
|
16378
17262
|
// --- Message Routing ---
|
|
17263
|
+
/**
|
|
17264
|
+
* Route an incoming platform message to the appropriate session.
|
|
17265
|
+
*
|
|
17266
|
+
* Flow:
|
|
17267
|
+
* 1. Run `message:incoming` middleware (plugins can modify or block)
|
|
17268
|
+
* 2. SecurityGuard checks user access and per-user session limits
|
|
17269
|
+
* 3. Find session by channel+thread (in-memory lookup, then lazy resume from disk)
|
|
17270
|
+
* 4. For assistant sessions, prepend any deferred system prompt
|
|
17271
|
+
* 5. Emit `message:queued` for SSE clients, then enqueue the prompt on the session
|
|
17272
|
+
*
|
|
17273
|
+
* If no session is found, the user is told to start one with /new.
|
|
17274
|
+
*/
|
|
16379
17275
|
async handleMessage(message) {
|
|
16380
17276
|
log16.debug(
|
|
16381
17277
|
{
|
|
@@ -16457,6 +17353,17 @@ ${text3}`;
|
|
|
16457
17353
|
}
|
|
16458
17354
|
}
|
|
16459
17355
|
// --- Unified Session Creation Pipeline ---
|
|
17356
|
+
/**
|
|
17357
|
+
* Create (or resume) a session with full wiring: agent, adapter thread, bridge, persistence.
|
|
17358
|
+
*
|
|
17359
|
+
* This is the single entry point for session creation. The pipeline:
|
|
17360
|
+
* 1. SessionFactory spawns/resumes the agent process
|
|
17361
|
+
* 2. Adapter creates a thread/topic if requested
|
|
17362
|
+
* 3. Initial session record is persisted (so lazy resume can find it by threadId)
|
|
17363
|
+
* 4. SessionBridge connects agent events to the adapter
|
|
17364
|
+
* 5. For headless sessions (no adapter), fallback event handlers are wired inline
|
|
17365
|
+
* 6. Side effects (usage tracking, tunnel cleanup) are attached
|
|
17366
|
+
*/
|
|
16460
17367
|
async createSession(params) {
|
|
16461
17368
|
const session = await this.sessionFactory.create(params);
|
|
16462
17369
|
if (params.threadId) {
|
|
@@ -16579,9 +17486,16 @@ ${text3}`;
|
|
|
16579
17486
|
);
|
|
16580
17487
|
return session;
|
|
16581
17488
|
}
|
|
17489
|
+
/** Convenience wrapper: create a new session with default agent/workspace resolution. */
|
|
16582
17490
|
async handleNewSession(channelId, agentName, workspacePath, options) {
|
|
16583
17491
|
return this.sessionFactory.handleNewSession(channelId, agentName, workspacePath, options);
|
|
16584
17492
|
}
|
|
17493
|
+
/**
|
|
17494
|
+
* Adopt an externally-started agent session (e.g. from a CLI `openacp adopt` command).
|
|
17495
|
+
*
|
|
17496
|
+
* Validates that the agent supports resume, checks session limits, avoids duplicates,
|
|
17497
|
+
* then creates a full session via the unified pipeline with resume semantics.
|
|
17498
|
+
*/
|
|
16585
17499
|
async adoptSession(agentName, agentSessionId, cwd, channelId) {
|
|
16586
17500
|
const caps = getAgentCapabilities(agentName);
|
|
16587
17501
|
if (!caps.supportsResume) {
|
|
@@ -16692,22 +17606,33 @@ ${text3}`;
|
|
|
16692
17606
|
status: "adopted"
|
|
16693
17607
|
};
|
|
16694
17608
|
}
|
|
17609
|
+
/** Start a new chat within the same agent and workspace as the current session's thread. */
|
|
16695
17610
|
async handleNewChat(channelId, currentThreadId) {
|
|
16696
17611
|
return this.sessionFactory.handleNewChat(channelId, currentThreadId);
|
|
16697
17612
|
}
|
|
17613
|
+
/** Create a session and inject conversation context from a prior session or repo. */
|
|
16698
17614
|
async createSessionWithContext(params) {
|
|
16699
17615
|
return this.sessionFactory.createSessionWithContext(params);
|
|
16700
17616
|
}
|
|
16701
17617
|
// --- Agent Switch ---
|
|
17618
|
+
/** Switch a session's active agent. Delegates to AgentSwitchHandler for state coordination. */
|
|
16702
17619
|
async switchSessionAgent(sessionId, toAgent) {
|
|
16703
17620
|
return this.agentSwitchHandler.switch(sessionId, toAgent);
|
|
16704
17621
|
}
|
|
17622
|
+
/** Find a session by channel+thread, resuming from disk if not in memory. */
|
|
16705
17623
|
async getOrResumeSession(channelId, threadId) {
|
|
16706
17624
|
return this.sessionFactory.getOrResume(channelId, threadId);
|
|
16707
17625
|
}
|
|
17626
|
+
/** Find a session by ID, resuming from disk if not in memory. */
|
|
16708
17627
|
async getOrResumeSessionById(sessionId) {
|
|
16709
17628
|
return this.sessionFactory.getOrResumeById(sessionId);
|
|
16710
17629
|
}
|
|
17630
|
+
/**
|
|
17631
|
+
* Attach an additional adapter to an existing session (multi-adapter support).
|
|
17632
|
+
*
|
|
17633
|
+
* Creates a thread on the target adapter and connects a SessionBridge so the
|
|
17634
|
+
* session's agent events are forwarded to both the primary and attached adapters.
|
|
17635
|
+
*/
|
|
16711
17636
|
async attachAdapter(sessionId, adapterId) {
|
|
16712
17637
|
const session = this.sessionManager.getSession(sessionId);
|
|
16713
17638
|
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
@@ -16731,6 +17656,10 @@ ${text3}`;
|
|
|
16731
17656
|
});
|
|
16732
17657
|
return { threadId };
|
|
16733
17658
|
}
|
|
17659
|
+
/**
|
|
17660
|
+
* Detach a secondary adapter from a session. The primary adapter (channelId) cannot
|
|
17661
|
+
* be detached. Disconnects the bridge and cleans up thread mappings.
|
|
17662
|
+
*/
|
|
16734
17663
|
async detachAdapter(sessionId, adapterId) {
|
|
16735
17664
|
const session = this.sessionManager.getSession(sessionId);
|
|
16736
17665
|
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
@@ -16763,6 +17692,7 @@ ${text3}`;
|
|
|
16763
17692
|
platforms: this.buildPlatformsFromSession(session)
|
|
16764
17693
|
});
|
|
16765
17694
|
}
|
|
17695
|
+
/** Build the platforms map (adapter → thread/topic IDs) for persistence. */
|
|
16766
17696
|
buildPlatformsFromSession(session) {
|
|
16767
17697
|
const platforms = {};
|
|
16768
17698
|
for (const [adapterId, threadId] of session.threadIds) {
|
|
@@ -16794,8 +17724,13 @@ ${text3}`;
|
|
|
16794
17724
|
const bridge = this.createBridge(session, adapter, session.channelId);
|
|
16795
17725
|
bridge.connect();
|
|
16796
17726
|
}
|
|
16797
|
-
/**
|
|
16798
|
-
*
|
|
17727
|
+
/**
|
|
17728
|
+
* Create a SessionBridge for the given session and adapter.
|
|
17729
|
+
*
|
|
17730
|
+
* The bridge subscribes to Session events (agent output, status changes, naming)
|
|
17731
|
+
* and forwards them to the adapter for platform delivery. Disconnects any existing
|
|
17732
|
+
* bridge for the same adapter+session first to avoid duplicate event handlers.
|
|
17733
|
+
*/
|
|
16799
17734
|
createBridge(session, adapter, adapterId) {
|
|
16800
17735
|
const id = adapterId ?? adapter.name;
|
|
16801
17736
|
const key = this.bridgeKey(id, session.id);
|
|
@@ -16899,10 +17834,15 @@ var CommandRegistry = class _CommandRegistry {
|
|
|
16899
17834
|
return this.getAll().filter((cmd) => cmd.category === category);
|
|
16900
17835
|
}
|
|
16901
17836
|
/**
|
|
16902
|
-
* Parse and execute a command string.
|
|
16903
|
-
*
|
|
16904
|
-
*
|
|
16905
|
-
*
|
|
17837
|
+
* Parse and execute a command string (e.g. "/greet hello world").
|
|
17838
|
+
*
|
|
17839
|
+
* Resolution order:
|
|
17840
|
+
* 1. Adapter-specific override (e.g. Telegram's version of /new)
|
|
17841
|
+
* 2. Short name or qualified name in the main registry
|
|
17842
|
+
*
|
|
17843
|
+
* Strips Telegram-style bot mentions (e.g. "/help@MyBot" → "help").
|
|
17844
|
+
* Returns `{ type: 'delegated' }` if the handler returns null/undefined
|
|
17845
|
+
* (meaning it handled the response itself, e.g. via assistant).
|
|
16906
17846
|
*/
|
|
16907
17847
|
async execute(commandString, baseArgs) {
|
|
16908
17848
|
const trimmed = commandString.trim();
|
|
@@ -18007,6 +18947,10 @@ var TopicManager = class {
|
|
|
18007
18947
|
this.adapter = adapter;
|
|
18008
18948
|
this.systemTopicIds = systemTopicIds;
|
|
18009
18949
|
}
|
|
18950
|
+
/**
|
|
18951
|
+
* List user-facing session topics, excluding system topics.
|
|
18952
|
+
* Optionally filtered to specific status values.
|
|
18953
|
+
*/
|
|
18010
18954
|
listTopics(filter) {
|
|
18011
18955
|
const records = this.sessionManager.listRecords(filter);
|
|
18012
18956
|
return records.filter((r) => !this.isSystemTopic(r)).filter((r) => !filter?.statuses?.length || filter.statuses.includes(r.status)).map((r) => ({
|
|
@@ -18018,6 +18962,12 @@ var TopicManager = class {
|
|
|
18018
18962
|
lastActiveAt: r.lastActiveAt
|
|
18019
18963
|
}));
|
|
18020
18964
|
}
|
|
18965
|
+
/**
|
|
18966
|
+
* Delete a session topic and its session record.
|
|
18967
|
+
*
|
|
18968
|
+
* Returns `needsConfirmation: true` when the session is still active and
|
|
18969
|
+
* `options.confirmed` was not set — callers must ask the user before proceeding.
|
|
18970
|
+
*/
|
|
18021
18971
|
async deleteTopic(sessionId, options) {
|
|
18022
18972
|
const records = this.sessionManager.listRecords();
|
|
18023
18973
|
const record = records.find((r) => r.sessionId === sessionId);
|
|
@@ -18045,6 +18995,10 @@ var TopicManager = class {
|
|
|
18045
18995
|
await this.sessionManager.removeRecord(sessionId);
|
|
18046
18996
|
return { ok: true, topicId };
|
|
18047
18997
|
}
|
|
18998
|
+
/**
|
|
18999
|
+
* Bulk-delete topics by status (default: finished, error, cancelled).
|
|
19000
|
+
* Active/initializing sessions are cancelled before deletion to prevent orphaned processes.
|
|
19001
|
+
*/
|
|
18048
19002
|
async cleanup(statuses) {
|
|
18049
19003
|
const targetStatuses = statuses?.length ? statuses : ["finished", "error", "cancelled"];
|
|
18050
19004
|
const records = this.sessionManager.listRecords({ statuses: targetStatuses });
|
|
@@ -18102,6 +19056,7 @@ var StreamAdapter = class {
|
|
|
18102
19056
|
...config
|
|
18103
19057
|
};
|
|
18104
19058
|
}
|
|
19059
|
+
/** Wraps the outgoing message as a StreamEvent and emits it to the session's listeners. */
|
|
18105
19060
|
async sendMessage(sessionId, content) {
|
|
18106
19061
|
await this.emit(sessionId, {
|
|
18107
19062
|
type: content.type,
|
|
@@ -18110,6 +19065,7 @@ var StreamAdapter = class {
|
|
|
18110
19065
|
timestamp: Date.now()
|
|
18111
19066
|
});
|
|
18112
19067
|
}
|
|
19068
|
+
/** Emits a permission request event so the client can render approve/deny UI. */
|
|
18113
19069
|
async sendPermissionRequest(sessionId, request) {
|
|
18114
19070
|
await this.emit(sessionId, {
|
|
18115
19071
|
type: "permission_request",
|
|
@@ -18118,6 +19074,7 @@ var StreamAdapter = class {
|
|
|
18118
19074
|
timestamp: Date.now()
|
|
18119
19075
|
});
|
|
18120
19076
|
}
|
|
19077
|
+
/** Broadcasts a notification to all connected clients (not scoped to a session). */
|
|
18121
19078
|
async sendNotification(notification) {
|
|
18122
19079
|
await this.broadcast({
|
|
18123
19080
|
type: "notification",
|
|
@@ -18125,9 +19082,14 @@ var StreamAdapter = class {
|
|
|
18125
19082
|
timestamp: Date.now()
|
|
18126
19083
|
});
|
|
18127
19084
|
}
|
|
19085
|
+
/**
|
|
19086
|
+
* No-op for stream adapters — threads are a platform concept (Telegram topics, Slack threads).
|
|
19087
|
+
* Stream clients manage their own session UI.
|
|
19088
|
+
*/
|
|
18128
19089
|
async createSessionThread(_sessionId, _name) {
|
|
18129
19090
|
return "";
|
|
18130
19091
|
}
|
|
19092
|
+
/** Emits a rename event so connected clients can update their session title. */
|
|
18131
19093
|
async renameSessionThread(sessionId, name) {
|
|
18132
19094
|
await this.emit(sessionId, {
|
|
18133
19095
|
type: "session_rename",
|
|
@@ -18152,20 +19114,27 @@ var Draft = class {
|
|
|
18152
19114
|
}
|
|
18153
19115
|
buffer = "";
|
|
18154
19116
|
_messageId;
|
|
19117
|
+
/** Guards against concurrent first-flush — ensures only one sendMessage creates the draft. */
|
|
18155
19118
|
firstFlushPending = false;
|
|
18156
19119
|
flushTimer;
|
|
18157
19120
|
flushPromise = Promise.resolve();
|
|
18158
19121
|
get isEmpty() {
|
|
18159
19122
|
return !this.buffer;
|
|
18160
19123
|
}
|
|
19124
|
+
/** Platform message ID, set after the first successful flush. */
|
|
18161
19125
|
get messageId() {
|
|
18162
19126
|
return this._messageId;
|
|
18163
19127
|
}
|
|
19128
|
+
/** Appends streaming text to the buffer and schedules a flush. */
|
|
18164
19129
|
append(text3) {
|
|
18165
19130
|
if (!text3) return;
|
|
18166
19131
|
this.buffer += text3;
|
|
18167
19132
|
this.scheduleFlush();
|
|
18168
19133
|
}
|
|
19134
|
+
/**
|
|
19135
|
+
* Flushes any remaining buffered text and returns the platform message ID.
|
|
19136
|
+
* Called when the streaming response completes.
|
|
19137
|
+
*/
|
|
18169
19138
|
async finalize() {
|
|
18170
19139
|
if (this.flushTimer) {
|
|
18171
19140
|
clearTimeout(this.flushTimer);
|
|
@@ -18177,6 +19146,7 @@ var Draft = class {
|
|
|
18177
19146
|
}
|
|
18178
19147
|
return this._messageId;
|
|
18179
19148
|
}
|
|
19149
|
+
/** Discards buffered text and cancels any pending flush. */
|
|
18180
19150
|
destroy() {
|
|
18181
19151
|
if (this.flushTimer) {
|
|
18182
19152
|
clearTimeout(this.flushTimer);
|
|
@@ -18219,6 +19189,7 @@ var DraftManager = class {
|
|
|
18219
19189
|
this.config = config;
|
|
18220
19190
|
}
|
|
18221
19191
|
drafts = /* @__PURE__ */ new Map();
|
|
19192
|
+
/** Returns the existing draft for a session, or creates a new one. */
|
|
18222
19193
|
getOrCreate(sessionId) {
|
|
18223
19194
|
let draft = this.drafts.get(sessionId);
|
|
18224
19195
|
if (!draft) {
|
|
@@ -18227,15 +19198,18 @@ var DraftManager = class {
|
|
|
18227
19198
|
}
|
|
18228
19199
|
return draft;
|
|
18229
19200
|
}
|
|
19201
|
+
/** Finalizes and removes the draft for a session. */
|
|
18230
19202
|
async finalize(sessionId) {
|
|
18231
19203
|
const draft = this.drafts.get(sessionId);
|
|
18232
19204
|
if (!draft) return;
|
|
18233
19205
|
await draft.finalize();
|
|
18234
19206
|
this.drafts.delete(sessionId);
|
|
18235
19207
|
}
|
|
19208
|
+
/** Finalizes all active drafts (e.g., during adapter shutdown). */
|
|
18236
19209
|
async finalizeAll() {
|
|
18237
19210
|
await Promise.all([...this.drafts.values()].map((d) => d.finalize()));
|
|
18238
19211
|
}
|
|
19212
|
+
/** Destroys a draft without flushing (e.g., on session error). */
|
|
18239
19213
|
destroy(sessionId) {
|
|
18240
19214
|
const draft = this.drafts.get(sessionId);
|
|
18241
19215
|
if (draft) {
|
|
@@ -18243,6 +19217,7 @@ var DraftManager = class {
|
|
|
18243
19217
|
this.drafts.delete(sessionId);
|
|
18244
19218
|
}
|
|
18245
19219
|
}
|
|
19220
|
+
/** Destroys all drafts without flushing. */
|
|
18246
19221
|
destroyAll() {
|
|
18247
19222
|
for (const draft of this.drafts.values()) {
|
|
18248
19223
|
draft.destroy();
|
|
@@ -18254,12 +19229,14 @@ var DraftManager = class {
|
|
|
18254
19229
|
// src/core/adapter-primitives/primitives/tool-call-tracker.ts
|
|
18255
19230
|
var ToolCallTracker = class {
|
|
18256
19231
|
sessions = /* @__PURE__ */ new Map();
|
|
19232
|
+
/** Registers a new tool call and associates it with its platform message ID. */
|
|
18257
19233
|
track(sessionId, meta, messageId) {
|
|
18258
19234
|
if (!this.sessions.has(sessionId)) {
|
|
18259
19235
|
this.sessions.set(sessionId, /* @__PURE__ */ new Map());
|
|
18260
19236
|
}
|
|
18261
19237
|
this.sessions.get(sessionId).set(meta.id, { ...meta, messageId });
|
|
18262
19238
|
}
|
|
19239
|
+
/** Updates a tracked tool call's status and optional metadata. Returns null if not found. */
|
|
18263
19240
|
update(sessionId, toolId, status, patch) {
|
|
18264
19241
|
const tool = this.sessions.get(sessionId)?.get(toolId);
|
|
18265
19242
|
if (!tool) return null;
|
|
@@ -18270,10 +19247,12 @@ var ToolCallTracker = class {
|
|
|
18270
19247
|
if (patch?.kind) tool.kind = patch.kind;
|
|
18271
19248
|
return tool;
|
|
18272
19249
|
}
|
|
19250
|
+
/** Returns all tracked tool calls for a session (regardless of status). */
|
|
18273
19251
|
getActive(sessionId) {
|
|
18274
19252
|
const session = this.sessions.get(sessionId);
|
|
18275
19253
|
return session ? [...session.values()] : [];
|
|
18276
19254
|
}
|
|
19255
|
+
/** Removes all tracked tool calls for a session (called at turn end). */
|
|
18277
19256
|
clear(sessionId) {
|
|
18278
19257
|
this.sessions.delete(sessionId);
|
|
18279
19258
|
}
|
|
@@ -18288,6 +19267,7 @@ var ActivityTracker = class {
|
|
|
18288
19267
|
this.config = config;
|
|
18289
19268
|
}
|
|
18290
19269
|
sessions = /* @__PURE__ */ new Map();
|
|
19270
|
+
/** Shows the typing indicator and starts the periodic refresh timer. */
|
|
18291
19271
|
onThinkingStart(sessionId, callbacks) {
|
|
18292
19272
|
this.cleanup(sessionId);
|
|
18293
19273
|
const state = {
|
|
@@ -18303,6 +19283,7 @@ var ActivityTracker = class {
|
|
|
18303
19283
|
this.startRefresh(sessionId, state);
|
|
18304
19284
|
}, 0);
|
|
18305
19285
|
}
|
|
19286
|
+
/** Dismisses the typing indicator when the agent starts producing text output. */
|
|
18306
19287
|
onTextStart(sessionId) {
|
|
18307
19288
|
const state = this.sessions.get(sessionId);
|
|
18308
19289
|
if (!state || state.dismissed) return;
|
|
@@ -18311,9 +19292,11 @@ var ActivityTracker = class {
|
|
|
18311
19292
|
state.callbacks.removeThinkingIndicator().catch(() => {
|
|
18312
19293
|
});
|
|
18313
19294
|
}
|
|
19295
|
+
/** Cleans up the typing indicator when the session ends. */
|
|
18314
19296
|
onSessionEnd(sessionId) {
|
|
18315
19297
|
this.cleanup(sessionId);
|
|
18316
19298
|
}
|
|
19299
|
+
/** Cleans up all sessions (e.g., during adapter shutdown). */
|
|
18317
19300
|
destroy() {
|
|
18318
19301
|
for (const [id] of this.sessions) {
|
|
18319
19302
|
this.cleanup(id);
|