comisai 1.0.36 → 1.0.37
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/node_modules/@comis/agent/dist/background/auto-background-middleware.js +9 -0
- package/node_modules/@comis/agent/dist/background/background-task-manager.d.ts +22 -2
- package/node_modules/@comis/agent/dist/background/background-task-manager.js +48 -41
- package/node_modules/@comis/agent/dist/background/background-task-persistence.js +28 -5
- package/node_modules/@comis/agent/dist/background/background-task-types.d.ts +49 -0
- package/node_modules/@comis/agent/dist/background/completion-dispatcher.d.ts +130 -0
- package/node_modules/@comis/agent/dist/background/completion-dispatcher.js +215 -0
- package/node_modules/@comis/agent/dist/background/completion-runner.d.ts +10 -1
- package/node_modules/@comis/agent/dist/background/completion-runner.js +98 -15
- package/node_modules/@comis/agent/dist/background/index.d.ts +6 -1
- package/node_modules/@comis/agent/dist/background/index.js +2 -0
- package/node_modules/@comis/agent/dist/background/session-resolver.d.ts +85 -0
- package/node_modules/@comis/agent/dist/background/session-resolver.js +78 -0
- package/node_modules/@comis/agent/dist/bootstrap/sections/messaging-sections.js +1 -0
- package/node_modules/@comis/agent/dist/bootstrap/sections/tool-descriptions.js +3 -3
- package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.d.ts +30 -2
- package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.js +51 -2
- package/node_modules/@comis/agent/dist/bootstrap/system-prompt-assembler.d.ts +22 -0
- package/node_modules/@comis/agent/dist/bootstrap/system-prompt-assembler.js +2 -2
- package/node_modules/@comis/agent/dist/bridge/bridge-event-handlers.d.ts +1 -5
- package/node_modules/@comis/agent/dist/bridge/bridge-event-handlers.js +2 -14
- package/node_modules/@comis/agent/dist/bridge/bridge-metrics.d.ts +26 -0
- package/node_modules/@comis/agent/dist/bridge/bridge-metrics.js +3 -0
- package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.d.ts +9 -0
- package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.js +73 -2
- package/node_modules/@comis/agent/dist/context-engine/signature-surrogate-guard.d.ts +10 -10
- package/node_modules/@comis/agent/dist/context-engine/signature-surrogate-guard.js +14 -14
- package/node_modules/@comis/agent/dist/context-engine/thinking-block-cleaner.d.ts +11 -13
- package/node_modules/@comis/agent/dist/context-engine/thinking-block-cleaner.js +14 -15
- package/node_modules/@comis/agent/dist/executor/capability-index-context.d.ts +72 -0
- package/node_modules/@comis/agent/dist/executor/capability-index-context.js +329 -0
- package/node_modules/@comis/agent/dist/executor/drain-helper.d.ts +122 -0
- package/node_modules/@comis/agent/dist/executor/drain-helper.js +173 -0
- package/node_modules/@comis/agent/dist/executor/error-classifier.js +2 -2
- package/node_modules/@comis/agent/dist/executor/executor-post-execution.d.ts +48 -4
- package/node_modules/@comis/agent/dist/executor/executor-post-execution.js +134 -31
- package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.d.ts +7 -0
- package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +25 -4
- package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.d.ts +18 -1
- package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +19 -16
- package/node_modules/@comis/agent/dist/executor/jit-guide-injector.d.ts +11 -2
- package/node_modules/@comis/agent/dist/executor/jit-guide-injector.js +16 -2
- package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +8 -2
- package/node_modules/@comis/agent/dist/executor/pi-executor.js +25 -12
- package/node_modules/@comis/agent/dist/executor/prompt-assembly.d.ts +9 -1
- package/node_modules/@comis/agent/dist/executor/prompt-assembly.js +15 -1
- package/node_modules/@comis/agent/dist/executor/tool-deferral.d.ts +18 -27
- package/node_modules/@comis/agent/dist/executor/tool-deferral.js +29 -38
- package/node_modules/@comis/agent/dist/model/model-registry-adapter.js +1 -1
- package/node_modules/@comis/agent/dist/model/model-scanner.js +1 -1
- package/node_modules/@comis/agent/dist/safety/tool-retry-breaker.d.ts +11 -1
- package/node_modules/@comis/agent/dist/safety/tool-retry-breaker.js +19 -22
- package/node_modules/@comis/agent/dist/session/comis-session-manager.d.ts +16 -2
- package/node_modules/@comis/agent/dist/spawn/pi-mono-adapters.d.ts +1 -1
- package/node_modules/@comis/agent/dist/spawn/pi-mono-adapters.js +5 -5
- package/node_modules/@comis/agent/dist/workspace/data-env.d.ts +38 -0
- package/node_modules/@comis/agent/dist/workspace/data-env.js +56 -0
- package/node_modules/@comis/agent/dist/workspace/index.d.ts +1 -0
- package/node_modules/@comis/agent/dist/workspace/index.js +1 -0
- package/node_modules/@comis/agent/dist/workspace/templates.js +5 -1
- package/node_modules/@comis/agent/package.json +1 -1
- package/node_modules/@comis/channels/dist/index.d.ts +1 -1
- package/node_modules/@comis/channels/dist/index.js +1 -1
- package/node_modules/@comis/channels/dist/shared/channel-manager.d.ts +9 -3
- package/node_modules/@comis/channels/dist/shared/inbound-gate.d.ts +1 -1
- package/node_modules/@comis/channels/dist/shared/inbound-gate.js +22 -7
- package/node_modules/@comis/channels/dist/shared/inbound-pipeline.d.ts +10 -3
- package/node_modules/@comis/channels/dist/shared/inbound-route.d.ts +1 -1
- package/node_modules/@comis/channels/dist/shared/inbound-route.js +13 -2
- package/node_modules/@comis/channels/dist/shared/response-filter.d.ts +11 -24
- package/node_modules/@comis/channels/dist/shared/response-filter.js +25 -53
- package/node_modules/@comis/channels/package.json +1 -1
- package/node_modules/@comis/cli/dist/commands/providers.d.ts +1 -2
- package/node_modules/@comis/cli/dist/commands/providers.js +5 -6
- package/node_modules/@comis/cli/package.json +1 -1
- package/node_modules/@comis/core/dist/config/field-metadata.js +2 -0
- package/node_modules/@comis/core/dist/config/immutable-keys.js +4 -1
- package/node_modules/@comis/core/dist/config/index.d.ts +4 -0
- package/node_modules/@comis/core/dist/config/index.js +2 -0
- package/node_modules/@comis/core/dist/config/schema-agent.d.ts +0 -792
- package/node_modules/@comis/core/dist/config/schema-approvals.d.ts +0 -14
- package/node_modules/@comis/core/dist/config/schema-auto-reply-engine.d.ts +0 -6
- package/node_modules/@comis/core/dist/config/schema-background-tasks.d.ts +0 -12
- package/node_modules/@comis/core/dist/config/schema-browser.d.ts +0 -18
- package/node_modules/@comis/core/dist/config/schema-channel.d.ts +0 -158
- package/node_modules/@comis/core/dist/config/schema-coalescer.d.ts +0 -5
- package/node_modules/@comis/core/dist/config/schema-daemon.d.ts +0 -32
- package/node_modules/@comis/core/dist/config/schema-delivery.d.ts +0 -18
- package/node_modules/@comis/core/dist/config/schema-documentation.d.ts +0 -12
- package/node_modules/@comis/core/dist/config/schema-embedding.d.ts +0 -20
- package/node_modules/@comis/core/dist/config/schema-envelope.d.ts +0 -15
- package/node_modules/@comis/core/dist/config/schema-gateway.d.ts +0 -37
- package/node_modules/@comis/core/dist/config/schema-gemini-cache.d.ts +0 -2
- package/node_modules/@comis/core/dist/config/schema-integrations.d.ts +0 -318
- package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.d.ts +0 -18
- package/node_modules/@comis/core/dist/config/schema-memory-review.d.ts +0 -7
- package/node_modules/@comis/core/dist/config/schema-memory.d.ts +0 -16
- package/node_modules/@comis/core/dist/config/schema-messages.d.ts +0 -8
- package/node_modules/@comis/core/dist/config/schema-models.d.ts +0 -15
- package/node_modules/@comis/core/dist/config/schema-notification.d.ts +0 -5
- package/node_modules/@comis/core/dist/config/schema-oauth.d.ts +0 -5
- package/node_modules/@comis/core/dist/config/schema-observability.d.ts +0 -38
- package/node_modules/@comis/core/dist/config/schema-output-retention.d.ts +34 -0
- package/node_modules/@comis/core/dist/config/schema-output-retention.js +48 -0
- package/node_modules/@comis/core/dist/config/schema-plugins.d.ts +0 -8
- package/node_modules/@comis/core/dist/config/schema-providers.d.ts +0 -64
- package/node_modules/@comis/core/dist/config/schema-queue.d.ts +0 -58
- package/node_modules/@comis/core/dist/config/schema-response-prefix.d.ts +0 -2
- package/node_modules/@comis/core/dist/config/schema-retry.d.ts +0 -6
- package/node_modules/@comis/core/dist/config/schema-scheduler.d.ts +0 -39
- package/node_modules/@comis/core/dist/config/schema-secrets.d.ts +0 -3
- package/node_modules/@comis/core/dist/config/schema-security.d.ts +0 -18
- package/node_modules/@comis/core/dist/config/schema-send-policy.d.ts +0 -13
- package/node_modules/@comis/core/dist/config/schema-sender-trust-display.d.ts +0 -5
- package/node_modules/@comis/core/dist/config/schema-serializer.js +2 -0
- package/node_modules/@comis/core/dist/config/schema-skills.d.ts +0 -61
- package/node_modules/@comis/core/dist/config/schema-streaming.d.ts +0 -38
- package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.d.ts +0 -3
- package/node_modules/@comis/core/dist/config/schema-tooling.d.ts +87 -0
- package/node_modules/@comis/core/dist/config/schema-tooling.js +152 -0
- package/node_modules/@comis/core/dist/config/schema-verbosity.d.ts +0 -12
- package/node_modules/@comis/core/dist/config/schema-webhooks.d.ts +0 -40
- package/node_modules/@comis/core/dist/config/schema.d.ts +41 -38
- package/node_modules/@comis/core/dist/config/schema.js +6 -0
- package/node_modules/@comis/core/dist/context/context.d.ts +0 -4
- package/node_modules/@comis/core/dist/domain/approval-request.d.ts +0 -17
- package/node_modules/@comis/core/dist/domain/background-task-origin.d.ts +0 -10
- package/node_modules/@comis/core/dist/domain/delivery-origin.d.ts +0 -5
- package/node_modules/@comis/core/dist/domain/execution-graph.d.ts +0 -48
- package/node_modules/@comis/core/dist/domain/memory-entry.d.ts +0 -3
- package/node_modules/@comis/core/dist/domain/model-compat.d.ts +0 -4
- package/node_modules/@comis/core/dist/domain/normalized-message.d.ts +0 -15
- package/node_modules/@comis/core/dist/domain/provider-capabilities.d.ts +0 -6
- package/node_modules/@comis/core/dist/domain/rich-message.d.ts +0 -14
- package/node_modules/@comis/core/dist/domain/subagent-context-config.d.ts +0 -22
- package/node_modules/@comis/core/dist/domain/subagent-context-types.d.ts +0 -8
- package/node_modules/@comis/core/dist/event-bus/events-agent.d.ts +31 -0
- package/node_modules/@comis/core/dist/event-bus/events-infra.d.ts +5 -0
- package/node_modules/@comis/core/dist/exports/config.d.ts +2 -2
- package/node_modules/@comis/core/dist/exports/config.js +3 -1
- package/node_modules/@comis/core/dist/exports/hooks.d.ts +1 -1
- package/node_modules/@comis/core/dist/exports/ports.d.ts +2 -2
- package/node_modules/@comis/core/dist/exports/ports.js +1 -1
- package/node_modules/@comis/core/dist/ports/channel-plugin.d.ts +0 -13
- package/node_modules/@comis/core/dist/ports/index.d.ts +2 -0
- package/node_modules/@comis/core/dist/ports/index.js +4 -0
- package/node_modules/@comis/core/dist/ports/no-op-tool-capability.d.ts +30 -0
- package/node_modules/@comis/core/dist/ports/no-op-tool-capability.js +47 -0
- package/node_modules/@comis/core/dist/ports/tool-capability.d.ts +165 -0
- package/node_modules/@comis/core/dist/ports/tool-capability.js +15 -0
- package/node_modules/@comis/core/dist/security/audit.d.ts +0 -11
- package/node_modules/@comis/core/dist/tool-metadata.d.ts +21 -1
- package/node_modules/@comis/core/dist/tool-metadata.js +1 -1
- package/node_modules/@comis/core/package.json +1 -1
- package/node_modules/@comis/daemon/bundled-skills/skill-creator/scripts/validate-skill.py +1 -1
- package/node_modules/@comis/daemon/dist/daemon.js +89 -14
- package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.d.ts +1 -1
- package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.js +1 -1
- package/node_modules/@comis/daemon/dist/rpc/builtin-provider-guard.js +2 -2
- package/node_modules/@comis/daemon/dist/rpc/credential-resolver.js +1 -1
- package/node_modules/@comis/daemon/dist/rpc/model-handlers.d.ts +1 -1
- package/node_modules/@comis/daemon/dist/rpc/model-handlers.js +2 -2
- package/node_modules/@comis/daemon/dist/sub-agent-runner.d.ts +18 -0
- package/node_modules/@comis/daemon/dist/sub-agent-runner.js +41 -9
- package/node_modules/@comis/daemon/dist/wiring/index.d.ts +2 -0
- package/node_modules/@comis/daemon/dist/wiring/index.js +1 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-agents.d.ts +36 -2
- package/node_modules/@comis/daemon/dist/wiring/setup-agents.js +45 -8
- package/node_modules/@comis/daemon/dist/wiring/setup-background-completion-runner.d.ts +28 -9
- package/node_modules/@comis/daemon/dist/wiring/setup-background-completion-runner.js +36 -9
- package/node_modules/@comis/daemon/dist/wiring/setup-background-tasks.js +2 -2
- package/node_modules/@comis/daemon/dist/wiring/setup-channels.d.ts +9 -2
- package/node_modules/@comis/daemon/dist/wiring/setup-channels.js +15 -9
- package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.d.ts +20 -5
- package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.js +20 -15
- package/node_modules/@comis/daemon/dist/wiring/setup-delivery.js +14 -2
- package/node_modules/@comis/daemon/dist/wiring/setup-gateway.d.ts +4 -6
- package/node_modules/@comis/daemon/dist/wiring/setup-gateway.js +3 -5
- package/node_modules/@comis/daemon/dist/wiring/setup-heartbeat.d.ts +20 -5
- package/node_modules/@comis/daemon/dist/wiring/setup-heartbeat.js +11 -2
- package/node_modules/@comis/daemon/dist/wiring/setup-output-retention.d.ts +89 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-output-retention.js +212 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-tools.d.ts +18 -4
- package/node_modules/@comis/daemon/dist/wiring/setup-tools.js +29 -10
- package/node_modules/@comis/daemon/dist/wiring/tool-capability-adapter.d.ts +75 -0
- package/node_modules/@comis/daemon/dist/wiring/tool-capability-adapter.js +253 -0
- package/node_modules/@comis/daemon/package.json +1 -1
- package/node_modules/@comis/gateway/dist/webhook/webhook-endpoint.d.ts +0 -4
- package/node_modules/@comis/gateway/package.json +1 -1
- package/node_modules/@comis/infra/package.json +1 -1
- package/node_modules/@comis/memory/package.json +1 -1
- package/node_modules/@comis/scheduler/dist/cron/cron-types.d.ts +0 -42
- package/node_modules/@comis/scheduler/dist/heartbeat/agent-heartbeat-source.d.ts +29 -8
- package/node_modules/@comis/scheduler/dist/heartbeat/agent-heartbeat-source.js +19 -7
- package/node_modules/@comis/scheduler/dist/system-events/system-event-types.d.ts +0 -3
- package/node_modules/@comis/scheduler/dist/tasks/task-types.d.ts +0 -17
- package/node_modules/@comis/scheduler/package.json +1 -1
- package/node_modules/@comis/shared/dist/index.d.ts +3 -0
- package/node_modules/@comis/shared/dist/index.js +4 -0
- package/node_modules/@comis/shared/dist/mcp-tool-name.d.ts +78 -0
- package/node_modules/@comis/shared/dist/mcp-tool-name.js +92 -0
- package/node_modules/@comis/shared/dist/silent-tokens.d.ts +38 -0
- package/node_modules/@comis/shared/dist/silent-tokens.js +51 -0
- package/node_modules/@comis/shared/dist/visible-delivery.d.ts +28 -0
- package/node_modules/@comis/shared/dist/visible-delivery.js +16 -0
- package/node_modules/@comis/shared/package.json +1 -1
- package/node_modules/@comis/skills/dist/bridge/mcp-tool-bridge.d.ts +2 -13
- package/node_modules/@comis/skills/dist/bridge/mcp-tool-bridge.js +3 -21
- package/node_modules/@comis/skills/dist/bridge/tool-metadata-enforcement.js +1 -1
- package/node_modules/@comis/skills/dist/bridge/tool-metadata-registry.js +4 -4
- package/node_modules/@comis/skills/dist/builtin/exec-tool.d.ts +55 -9
- package/node_modules/@comis/skills/dist/builtin/exec-tool.js +383 -19
- package/node_modules/@comis/skills/dist/builtin/install-detour.d.ts +67 -0
- package/node_modules/@comis/skills/dist/builtin/install-detour.js +342 -0
- package/node_modules/@comis/skills/dist/builtin/platform/admin-manage-factory.js +5 -5
- package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.d.ts +2 -2
- package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.js +2 -2
- package/node_modules/@comis/skills/dist/builtin/platform/message-tool.js +18 -0
- package/node_modules/@comis/skills/dist/builtin/platform/messaging-factory.d.ts +18 -1
- package/node_modules/@comis/skills/dist/builtin/platform/messaging-factory.js +18 -2
- package/node_modules/@comis/skills/dist/builtin/platform/models-manage-tool.js +3 -3
- package/node_modules/@comis/skills/dist/builtin/process-registry.d.ts +14 -0
- package/node_modules/@comis/skills/dist/builtin/process-tool.d.ts +24 -4
- package/node_modules/@comis/skills/dist/builtin/process-tool.js +25 -7
- package/node_modules/@comis/skills/dist/builtin/sandbox/bwrap-provider.d.ts +1 -1
- package/node_modules/@comis/skills/dist/builtin/sandbox/bwrap-provider.js +9 -0
- package/node_modules/@comis/skills/dist/index.d.ts +4 -1
- package/node_modules/@comis/skills/dist/index.js +3 -1
- package/node_modules/@comis/skills/dist/manifest/capability-parser.d.ts +44 -0
- package/node_modules/@comis/skills/dist/manifest/capability-parser.js +68 -0
- package/node_modules/@comis/skills/dist/manifest/schema.d.ts +44 -37
- package/node_modules/@comis/skills/dist/manifest/schema.js +35 -0
- package/node_modules/@comis/skills/dist/registry/discovery.d.ts +8 -0
- package/node_modules/@comis/skills/dist/registry/discovery.js +10 -3
- package/node_modules/@comis/skills/dist/registry/skill-registry.d.ts +45 -1
- package/node_modules/@comis/skills/dist/registry/skill-registry.js +70 -7
- package/node_modules/@comis/skills/package.json +1 -1
- package/node_modules/@comis/web/package.json +1 -1
- package/package.json +21 -21
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Completion dispatcher: routes background_task:completed/failed events
|
|
4
|
+
* through the BackgroundSessionState machine.
|
|
5
|
+
*
|
|
6
|
+
* Subscribes to background_task:completed and background_task:failed BEFORE
|
|
7
|
+
* the existing BackgroundCompletionRunner. On each event:
|
|
8
|
+
* 1. Reads `task.dispatchState`.
|
|
9
|
+
* 2. If "pending": transitions to "notified" only when the runner cannot
|
|
10
|
+
* re-enter the originating session (no active session for the formatted
|
|
11
|
+
* key, or recursion limit reached). Otherwise transitions to "dispatched"
|
|
12
|
+
* and lets the completion-runner perform re-entry.
|
|
13
|
+
* 3. If already "notified" or "dispatched": no-op (at-most-once).
|
|
14
|
+
*
|
|
15
|
+
* The runner is wired AFTER the dispatcher in setup-background-completion-
|
|
16
|
+
* runner.ts so its handler reads the updated `task.dispatchState` and skips
|
|
17
|
+
* its own work when state is "notified" (the dispatcher already fired
|
|
18
|
+
* fallback). This single-owner contract ensures the completion runner does
|
|
19
|
+
* not double-fire user-visible notifications: the dispatcher routes via
|
|
20
|
+
* persistent state instead of an in-memory event handler, and gates on
|
|
21
|
+
* state instead of unconditionally firing.
|
|
22
|
+
*
|
|
23
|
+
* **State persistence:** every transition calls `manager.transitionDispatch
|
|
24
|
+
* State(taskId, next)` (when the manager exposes it) which mutates the
|
|
25
|
+
* in-memory task AND calls persistTaskSync. Recovery-after-SIGKILL reads
|
|
26
|
+
* the persisted state and the manager skips re-emitting completion events
|
|
27
|
+
* for already-dispatched / already-notified tasks.
|
|
28
|
+
*
|
|
29
|
+
* **Failure isolation:** each handler is wrapped in suppressError so a
|
|
30
|
+
* single dispatch's failure does not tear down the subscription
|
|
31
|
+
* (AGENTS §2.1).
|
|
32
|
+
*
|
|
33
|
+
* @module
|
|
34
|
+
*/
|
|
35
|
+
import { suppressError } from "@comis/shared";
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Runtime constants exported for downstream consumers (test surface + ops).
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
/**
|
|
40
|
+
* The 3-state typed enum as a runtime array. Order matches transition order:
|
|
41
|
+
* pending → (notified | dispatched).
|
|
42
|
+
*
|
|
43
|
+
* Exported as a `readonly string[]` so tests can assert
|
|
44
|
+
* `STATES === ["pending", "notified", "dispatched"]`.
|
|
45
|
+
*/
|
|
46
|
+
export const STATES = [
|
|
47
|
+
"pending",
|
|
48
|
+
"notified",
|
|
49
|
+
"dispatched",
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Notification policy as a runtime object so it round-trips through
|
|
53
|
+
* JSON.parse(JSON.stringify(...)) preserving identity. A boolean would
|
|
54
|
+
* collapse to true/false on rehydrate and lose the distinction between
|
|
55
|
+
* "deferred" / "immediate" / "silent".
|
|
56
|
+
*
|
|
57
|
+
* The typed enum is the single source of truth. This runtime object is a
|
|
58
|
+
* discoverability surface (tests, debugging, logs); production code uses
|
|
59
|
+
* the type-only `BackgroundTaskNotificationPolicy` from
|
|
60
|
+
* `background-task-types.ts`.
|
|
61
|
+
*/
|
|
62
|
+
export const BackgroundTaskNotificationPolicy = {
|
|
63
|
+
DEFERRED: "deferred",
|
|
64
|
+
IMMEDIATE: "immediate",
|
|
65
|
+
SILENT: "silent",
|
|
66
|
+
};
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Factory
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
/**
|
|
71
|
+
* Wire the completion dispatcher against an event bus + task manager.
|
|
72
|
+
* Subscriptions are installed synchronously; call shutdown() to remove them.
|
|
73
|
+
*
|
|
74
|
+
* At-most-once fallback: the state-machine transitions on
|
|
75
|
+
* `task.dispatchState` are the single source of truth. The dispatcher's
|
|
76
|
+
* synchronous transitionDispatchState runs BEFORE the completion-runner's
|
|
77
|
+
* handler reads the updated state, by virtue of the event-bus subscribing
|
|
78
|
+
* the dispatcher first (see setup-background-completion-runner.ts).
|
|
79
|
+
*/
|
|
80
|
+
export function createCompletionDispatcher(deps) {
|
|
81
|
+
const log = deps.logger.child({ submodule: "completion-dispatcher" });
|
|
82
|
+
const fallback = deps.fallbackNotifyFn ?? deps.notifyFn;
|
|
83
|
+
let stopped = false;
|
|
84
|
+
let inflight = Promise.resolve();
|
|
85
|
+
const onCompleted = (data) => {
|
|
86
|
+
if (stopped)
|
|
87
|
+
return;
|
|
88
|
+
const promise = handleEvent(data.taskId, "completed");
|
|
89
|
+
inflight = inflight.then(() => promise).catch(() => undefined);
|
|
90
|
+
suppressError(promise, "completion dispatcher (completed)");
|
|
91
|
+
};
|
|
92
|
+
const onFailed = (data) => {
|
|
93
|
+
if (stopped)
|
|
94
|
+
return;
|
|
95
|
+
const promise = handleEvent(data.taskId, "failed");
|
|
96
|
+
inflight = inflight.then(() => promise).catch(() => undefined);
|
|
97
|
+
suppressError(promise, "completion dispatcher (failed)");
|
|
98
|
+
};
|
|
99
|
+
deps.eventBus.on("background_task:completed", onCompleted);
|
|
100
|
+
deps.eventBus.on("background_task:failed", onFailed);
|
|
101
|
+
async function handleEvent(taskId, kind) {
|
|
102
|
+
const task = deps.taskManager.getTask(taskId);
|
|
103
|
+
if (!task) {
|
|
104
|
+
log.debug({ taskId, kind, hint: "Task disappeared from manager before dispatcher could resolve it" }, "Completion dispatcher: task not in manager");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const current = task.dispatchState ?? "pending";
|
|
108
|
+
// At-most-once: state machine is the single source of truth.
|
|
109
|
+
if (current === "notified" || current === "dispatched") {
|
|
110
|
+
log.debug({
|
|
111
|
+
taskId,
|
|
112
|
+
dispatchState: current,
|
|
113
|
+
// traceId from task.origin so dispatcher logs stay threaded with
|
|
114
|
+
// the originating request even when the dispatcher runs from a
|
|
115
|
+
// background ALS context.
|
|
116
|
+
traceId: task.origin?.traceId ?? undefined,
|
|
117
|
+
hint: "Task already dispatched/notified; no-op (at-most-once)",
|
|
118
|
+
}, "Completion dispatcher: at-most-once gate");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// task.dispatchState === "pending". Decide which transition to make.
|
|
122
|
+
// origin is producer-required; read it directly.
|
|
123
|
+
const origin = task.origin;
|
|
124
|
+
// Hop cap (when configured). Recursion limit reached → fallback.
|
|
125
|
+
if (typeof deps.maxBackgroundHops === "number") {
|
|
126
|
+
const nextHopCount = (origin.backgroundHopCount ?? 0) + 1;
|
|
127
|
+
if (nextHopCount >= deps.maxBackgroundHops) {
|
|
128
|
+
transitionTo(taskId, "notified");
|
|
129
|
+
await fireFallback(task, `Background task "${task.toolName}" completed but follow-up was skipped — recursion limit reached. Run again or check the result manually.`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Active-session check (when configured). No active session → fallback.
|
|
134
|
+
if (deps.sessionStore) {
|
|
135
|
+
const sessionExists = deps.sessionStore.loadByFormattedKey(origin.sessionKey) !== undefined;
|
|
136
|
+
if (!sessionExists) {
|
|
137
|
+
// The originating session is not currently registered. The
|
|
138
|
+
// completion-runner would skip re-entry (no streaming channel) —
|
|
139
|
+
// fire fallback so the user still sees a notification. The
|
|
140
|
+
// dispatcher transitions to "notified" so the runner does NOT
|
|
141
|
+
// also fire (single-owner contract).
|
|
142
|
+
transitionTo(taskId, "notified");
|
|
143
|
+
await fireFallback(task, `Background task "${task.toolName}" completed.`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Active session exists (or sessionStore not wired): the runner will
|
|
148
|
+
// dispatch via re-entry. Transition to "dispatched" so the runner's
|
|
149
|
+
// handler — which reads task.dispatchState — sees the updated state.
|
|
150
|
+
// We do NOT fire fallback here (zero spurious outbound).
|
|
151
|
+
transitionTo(taskId, "dispatched");
|
|
152
|
+
log.debug({
|
|
153
|
+
taskId,
|
|
154
|
+
sessionKey: origin.sessionKey,
|
|
155
|
+
agentId: origin.agentId,
|
|
156
|
+
toolName: task.toolName,
|
|
157
|
+
// traceId from origin for log continuity.
|
|
158
|
+
traceId: origin.traceId ?? undefined,
|
|
159
|
+
hint: "Runner will re-enter the originating session",
|
|
160
|
+
}, "Completion dispatcher: marked dispatched");
|
|
161
|
+
}
|
|
162
|
+
function transitionTo(taskId, next) {
|
|
163
|
+
if (typeof deps.taskManager.transitionDispatchState === "function") {
|
|
164
|
+
deps.taskManager.transitionDispatchState(taskId, next);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// No persistent transition wired — mutate the in-memory task directly so
|
|
168
|
+
// the runner (which receives the same event in the same tick) reads the
|
|
169
|
+
// updated state. Test fixtures take this branch.
|
|
170
|
+
const task = deps.taskManager.getTask(taskId);
|
|
171
|
+
if (task)
|
|
172
|
+
task.dispatchState = next;
|
|
173
|
+
}
|
|
174
|
+
async function fireFallback(task, message) {
|
|
175
|
+
if (!fallback) {
|
|
176
|
+
log.debug({
|
|
177
|
+
taskId: task.id,
|
|
178
|
+
// traceId from origin keeps log lines threaded.
|
|
179
|
+
traceId: task.origin?.traceId ?? undefined,
|
|
180
|
+
hint: "No fallbackNotifyFn wired; dispatcher cannot fire user-visible notification",
|
|
181
|
+
}, "Completion dispatcher: fallback skipped (no notifyFn)");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await fallback({
|
|
186
|
+
agentId: task.origin.agentId,
|
|
187
|
+
message,
|
|
188
|
+
priority: "normal",
|
|
189
|
+
origin: "background_task",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
log.warn({
|
|
194
|
+
taskId: task.id,
|
|
195
|
+
agentId: task.origin.agentId,
|
|
196
|
+
err,
|
|
197
|
+
// traceId from origin keeps the WARN line threaded.
|
|
198
|
+
traceId: task.origin?.traceId ?? undefined,
|
|
199
|
+
hint: "fallbackNotifyFn rejected; user will not see the completion notification for this task",
|
|
200
|
+
errorKind: "internal",
|
|
201
|
+
}, "Completion dispatcher: fallbackNotifyFn rejected");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
async shutdown() {
|
|
206
|
+
if (stopped)
|
|
207
|
+
return;
|
|
208
|
+
stopped = true;
|
|
209
|
+
deps.eventBus.off("background_task:completed", onCompleted);
|
|
210
|
+
deps.eventBus.off("background_task:failed", onFailed);
|
|
211
|
+
// Wait for any in-flight handler to settle before returning.
|
|
212
|
+
await inflight;
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -41,7 +41,16 @@ export interface BackgroundCompletionRunnerDeps {
|
|
|
41
41
|
eventBus: TypedEventBus;
|
|
42
42
|
getExecutor: (agentId: string) => AgentExecutor;
|
|
43
43
|
sessionStore: RunnerSessionStore;
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Includes `transitionDispatchState` in addition to `getTask`. fallbackForTask
|
|
46
|
+
* uses transitionDispatchState to persist `dispatchState = "notified"` BEFORE
|
|
47
|
+
* firing `fallbackNotifyFn`, so a daemon SIGKILL between persist and fire does
|
|
48
|
+
* NOT leak a duplicate notification on recovery (the at-most-once gate binds
|
|
49
|
+
* against on-disk state). The daemon-side wiring at
|
|
50
|
+
* setup-background-completion-runner.ts already passes a manager with
|
|
51
|
+
* both methods.
|
|
52
|
+
*/
|
|
53
|
+
taskManager: Pick<BackgroundTaskManager, "getTask" | "transitionDispatchState">;
|
|
45
54
|
fallbackNotifyFn: NotifyFn;
|
|
46
55
|
maxBackgroundHops: number;
|
|
47
56
|
logger: ComisLogger;
|
|
@@ -58,32 +58,72 @@ export function createBackgroundCompletionRunner(deps) {
|
|
|
58
58
|
log.warn({ taskId, kind, hint: "Task disappeared from manager before runner could resolve it; no announcement injected", errorKind: "internal" }, "Background completion: task not in manager");
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
// At-most-once: the dispatcher subscribed BEFORE this runner (see
|
|
62
|
+
// setup-background-completion-runner.ts) and already transitioned
|
|
63
|
+
// task.dispatchState. When state is "notified", the dispatcher fired
|
|
64
|
+
// the user-visible fallback; the runner stays out of the way to
|
|
65
|
+
// enforce single-owner notification routing (zero spurious outbound).
|
|
66
|
+
if (task.dispatchState === "notified") {
|
|
67
|
+
log.debug({
|
|
68
|
+
taskId,
|
|
69
|
+
dispatchState: task.dispatchState,
|
|
70
|
+
// Include originating traceId so operator log streams stay
|
|
71
|
+
// continuous across the dispatcher / runner boundary.
|
|
72
|
+
traceId: task.origin?.traceId ?? undefined,
|
|
73
|
+
hint: "Dispatcher already fired fallback notification (at-most-once)",
|
|
74
|
+
}, "Background completion runner: skipped (dispatcher fired fallback)");
|
|
65
75
|
return;
|
|
66
76
|
}
|
|
77
|
+
// origin is producer-required (background-task-manager promote()
|
|
78
|
+
// rejects missing-origin) so we read it directly.
|
|
79
|
+
const origin = task.origin;
|
|
67
80
|
// Hop cap. Read incoming hop count from origin (schema field populated
|
|
68
81
|
// by the originResolver).
|
|
69
82
|
const nextHopCount = (origin.backgroundHopCount ?? 0) + 1;
|
|
70
83
|
if (nextHopCount >= deps.maxBackgroundHops) {
|
|
71
|
-
log.info({
|
|
72
|
-
|
|
84
|
+
log.info({
|
|
85
|
+
taskId,
|
|
86
|
+
toolName: task.toolName,
|
|
87
|
+
agentId: origin.agentId,
|
|
88
|
+
hopCount: nextHopCount,
|
|
89
|
+
max: deps.maxBackgroundHops,
|
|
90
|
+
// traceId from origin keeps operator logs threaded.
|
|
91
|
+
traceId: origin.traceId ?? undefined,
|
|
92
|
+
}, "Background completion: hop cap reached, falling back to user notification");
|
|
93
|
+
await fallbackForTask(task.id, origin.agentId, task.toolName, `Background task "${task.toolName}" completed but follow-up was skipped — recursion limit reached. Run again or check the result manually.`);
|
|
73
94
|
return;
|
|
74
95
|
}
|
|
75
|
-
//
|
|
76
|
-
//
|
|
96
|
+
// No active session for this sessionKey in the in-memory store. The
|
|
97
|
+
// originating session may have ended (user closed the channel) OR may live
|
|
98
|
+
// in JSONL but not be currently registered. Either way, there is no
|
|
99
|
+
// streaming channel to inject into, so skip fallback (which would only
|
|
100
|
+
// produce a WARN from notification-service).
|
|
77
101
|
const sessionExists = deps.sessionStore.loadByFormattedKey(origin.sessionKey) !== undefined;
|
|
78
102
|
if (!sessionExists) {
|
|
79
|
-
log.info({
|
|
103
|
+
log.info({
|
|
104
|
+
taskId,
|
|
105
|
+
sessionKey: origin.sessionKey,
|
|
106
|
+
// traceId from origin so this INFO log line stays threaded with the
|
|
107
|
+
// originating request's trace stream even though the runner runs in a
|
|
108
|
+
// background context (the ALS traceId at this point may differ from
|
|
109
|
+
// origin.traceId).
|
|
110
|
+
traceId: origin.traceId ?? undefined,
|
|
111
|
+
hint: "No active in-memory session for this sessionKey; runner will skip re-entry. Task result remains in JSONL for offline review.",
|
|
112
|
+
}, "Background completion: no active session for re-entry");
|
|
80
113
|
return;
|
|
81
114
|
}
|
|
82
115
|
// Reconstruct the SessionKey object for executor.execute().
|
|
83
116
|
const parsedKey = parseFormattedSessionKey(origin.sessionKey);
|
|
84
117
|
if (!parsedKey) {
|
|
85
|
-
log.warn({
|
|
86
|
-
|
|
118
|
+
log.warn({
|
|
119
|
+
taskId,
|
|
120
|
+
sessionKey: origin.sessionKey,
|
|
121
|
+
// traceId from origin keeps operator logs threaded.
|
|
122
|
+
traceId: origin.traceId ?? undefined,
|
|
123
|
+
hint: "Persisted sessionKey is malformed; cannot route announcement",
|
|
124
|
+
errorKind: "internal",
|
|
125
|
+
}, "Background completion: invalid sessionKey");
|
|
126
|
+
await fallbackForTask(task.id, origin.agentId, task.toolName, `Background task "${task.toolName}" completed (routing failed).`);
|
|
87
127
|
return;
|
|
88
128
|
}
|
|
89
129
|
// Format the announcement (byte-identical trailing instruction).
|
|
@@ -105,15 +145,27 @@ export function createBackgroundCompletionRunner(deps) {
|
|
|
105
145
|
traceId: origin.traceId ?? undefined,
|
|
106
146
|
},
|
|
107
147
|
};
|
|
108
|
-
log.debug({
|
|
148
|
+
log.debug({
|
|
149
|
+
taskId,
|
|
150
|
+
sessionKey: origin.sessionKey,
|
|
151
|
+
agentId: origin.agentId,
|
|
152
|
+
toolName: task.toolName,
|
|
153
|
+
hopCount: nextHopCount,
|
|
154
|
+
// traceId from origin keeps debug logs threaded.
|
|
155
|
+
traceId: origin.traceId ?? undefined,
|
|
156
|
+
}, "Background completion runner: invoking executor");
|
|
109
157
|
// Emit background_task:reentered immediately before executor.execute().
|
|
110
158
|
// Integration tests compute p95 latency from
|
|
111
159
|
// background_task:completed.timestamp to this event's timestamp.
|
|
160
|
+
// Include traceId from origin so subscribers (and operator log streams)
|
|
161
|
+
// preserve the originating request's trace across the
|
|
162
|
+
// background_task:completed → :reentered boundary.
|
|
112
163
|
deps.eventBus.emit("background_task:reentered", {
|
|
113
164
|
taskId: task.id,
|
|
114
165
|
agentId: origin.agentId,
|
|
115
166
|
sessionKey: origin.sessionKey,
|
|
116
167
|
hopCount: nextHopCount,
|
|
168
|
+
traceId: origin.traceId ?? null,
|
|
117
169
|
timestamp: Date.now(),
|
|
118
170
|
});
|
|
119
171
|
// One turn per event. Existing session lock orders concurrent calls.
|
|
@@ -121,10 +173,41 @@ export function createBackgroundCompletionRunner(deps) {
|
|
|
121
173
|
await deps.getExecutor(origin.agentId).execute(syntheticMsg, parsedKey, undefined, undefined, origin.agentId);
|
|
122
174
|
}
|
|
123
175
|
catch (err) {
|
|
124
|
-
log.warn({
|
|
176
|
+
log.warn({
|
|
177
|
+
taskId,
|
|
178
|
+
err,
|
|
179
|
+
// traceId from origin keeps the WARN line threaded.
|
|
180
|
+
traceId: origin.traceId ?? undefined,
|
|
181
|
+
hint: "Executor failed mid-completion turn; subscription remains active",
|
|
182
|
+
errorKind: "internal",
|
|
183
|
+
}, "Background completion: executor.execute() rejected");
|
|
125
184
|
}
|
|
126
185
|
}
|
|
127
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Two-phase commit:
|
|
188
|
+
*
|
|
189
|
+
* 1. transitionDispatchState(taskId, "notified") — synchronously persists
|
|
190
|
+
* `dispatchState = "notified"` to disk (via persistTaskSync inside the
|
|
191
|
+
* manager). This MUST run before fallbackNotifyFn so a SIGKILL between
|
|
192
|
+
* persist and fire does NOT leak a duplicate on recovery: the at-most-
|
|
193
|
+
* once gate at the top of handleEvent (which reads task.dispatchState)
|
|
194
|
+
* sees "notified" and skips re-firing. Without this ordering, the gate
|
|
195
|
+
* misses and the user receives the notification twice.
|
|
196
|
+
*
|
|
197
|
+
* 2. fallbackNotifyFn(...) — actually deliver the user-visible
|
|
198
|
+
* notification. May reject (channel offline, rate-limited, etc.); the
|
|
199
|
+
* failure is logged at WARN. The persisted state stays at "notified" —
|
|
200
|
+
* the user did not see the notification, but the at-most-once contract
|
|
201
|
+
* takes precedence over delivery completeness.
|
|
202
|
+
*/
|
|
203
|
+
async function fallbackForTask(taskId, agentId, toolName, message) {
|
|
204
|
+
// Phase 1: persist state. transitionDispatchState may return false if
|
|
205
|
+
// the task disappeared from the manager between handler entry and this
|
|
206
|
+
// call (e.g., explicit cleanup). In that case there is nothing to gate
|
|
207
|
+
// on; we still fire so the user sees the completion. The persist is
|
|
208
|
+
// synchronous so the on-disk state is updated before phase 2.
|
|
209
|
+
deps.taskManager.transitionDispatchState(taskId, "notified");
|
|
210
|
+
// Phase 2: fire user-visible notification.
|
|
128
211
|
try {
|
|
129
212
|
await deps.fallbackNotifyFn({
|
|
130
213
|
agentId,
|
|
@@ -134,7 +217,7 @@ export function createBackgroundCompletionRunner(deps) {
|
|
|
134
217
|
});
|
|
135
218
|
}
|
|
136
219
|
catch (err) {
|
|
137
|
-
log.warn({ toolName, agentId, err, hint: "fallbackNotifyFn rejected; user will not see the completion notification for this task", errorKind: "internal" }, "Background completion: fallbackNotifyFn rejected");
|
|
220
|
+
log.warn({ taskId, toolName, agentId, err, hint: "fallbackNotifyFn rejected; user will not see the completion notification for this task. dispatchState already persisted as \"notified\" — no duplicate on recovery.", errorKind: "internal" }, "Background completion: fallbackNotifyFn rejected (post-persist)");
|
|
138
221
|
}
|
|
139
222
|
}
|
|
140
223
|
return {
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
-
export type { BackgroundTask, BackgroundTaskStatus, PersistedTaskState } from "./background-task-types.js";
|
|
6
|
+
export type { BackgroundTask, BackgroundTaskStatus, BackgroundSessionState, PersistedTaskState, } from "./background-task-types.js";
|
|
7
|
+
export type { BackgroundTaskNotificationPolicy as BackgroundTaskNotificationPolicyType } from "./background-task-types.js";
|
|
7
8
|
export type { BackgroundTaskOrigin } from "@comis/core";
|
|
8
9
|
export { persistTaskSync, loadTask, recoverTasks, removeTaskFile, TASK_DIR_NAME, } from "./background-task-persistence.js";
|
|
9
10
|
export { createBackgroundTaskManager, } from "./background-task-manager.js";
|
|
@@ -13,3 +14,7 @@ export type { ToolDefinition, } from "./auto-background-middleware.js";
|
|
|
13
14
|
export { formatCompletionAnnouncement, TRAILING_INSTRUCTION } from "./completion-formatter.js";
|
|
14
15
|
export { createBackgroundCompletionRunner } from "./completion-runner.js";
|
|
15
16
|
export type { BackgroundCompletionRunner, BackgroundCompletionRunnerDeps, RunnerSessionStore, } from "./completion-runner.js";
|
|
17
|
+
export { createCompletionDispatcher, STATES, BackgroundTaskNotificationPolicy, } from "./completion-dispatcher.js";
|
|
18
|
+
export type { CompletionDispatcher, CompletionDispatcherDeps, DispatcherSessionStore, DispatcherTaskManager, } from "./completion-dispatcher.js";
|
|
19
|
+
export { createBackgroundSessionResolver } from "./session-resolver.js";
|
|
20
|
+
export type { ActiveSessionKey, BackgroundSessionResolver, BackgroundSessionResolverDeps, } from "./session-resolver.js";
|
|
@@ -9,3 +9,5 @@ export { createBackgroundTaskManager, } from "./background-task-manager.js";
|
|
|
9
9
|
export { wrapToolForAutoBackground, } from "./auto-background-middleware.js";
|
|
10
10
|
export { formatCompletionAnnouncement, TRAILING_INSTRUCTION } from "./completion-formatter.js";
|
|
11
11
|
export { createBackgroundCompletionRunner } from "./completion-runner.js";
|
|
12
|
+
export { createCompletionDispatcher, STATES, BackgroundTaskNotificationPolicy, } from "./completion-dispatcher.js";
|
|
13
|
+
export { createBackgroundSessionResolver } from "./session-resolver.js";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BackgroundSessionResolver: composite-key wrapper around ActiveRunRegistry.
|
|
3
|
+
*
|
|
4
|
+
* The underlying `activeRunRegistry.has(sessionKey)` and `.get(sessionKey)`
|
|
5
|
+
* take a single formatted-key string. That string would collapse two
|
|
6
|
+
* distinct sessions for the same `channelId` across different agents (or
|
|
7
|
+
* different channelTypes) into one bucket — a latent multi-agent /
|
|
8
|
+
* multi-channel correctness bug.
|
|
9
|
+
*
|
|
10
|
+
* This resolver makes the composite key explicit at every public call site:
|
|
11
|
+
* `(agentId, channelType, channelId)`. It internally composes the formatted
|
|
12
|
+
* key via `formatSessionKey` from `@comis/core` and delegates to the
|
|
13
|
+
* underlying registry.
|
|
14
|
+
*
|
|
15
|
+
* Runtime semantics do not change at the registry layer — what changes is
|
|
16
|
+
* the lookup-key signature surfaced to production callers. No production
|
|
17
|
+
* code outside *.test.ts should retain a single-arg `.has(...)` or
|
|
18
|
+
* `.get(...)` on `activeRunRegistry`.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import type { ActiveRunRegistry, RunHandle } from "../executor/active-run-registry.js";
|
|
23
|
+
/**
|
|
24
|
+
* Composite key for active-session lookup.
|
|
25
|
+
*
|
|
26
|
+
* The three fields uniquely identify a session at the inbound-routing layer:
|
|
27
|
+
* - `agentId` — distinguishes per-agent isolation (multi-agent safety)
|
|
28
|
+
* - `channelType` — distinguishes platform (telegram vs discord vs slack)
|
|
29
|
+
* - `channelId` — platform-specific chat / peer / group identifier
|
|
30
|
+
*
|
|
31
|
+
* The resolver internally composes a `SessionKey` and formats it via
|
|
32
|
+
* `formatSessionKey` so the underlying registry's string-keyed Map is
|
|
33
|
+
* addressed deterministically.
|
|
34
|
+
*/
|
|
35
|
+
export interface ActiveSessionKey {
|
|
36
|
+
agentId: string;
|
|
37
|
+
channelType: string;
|
|
38
|
+
channelId: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Public-facing resolver returned by `createBackgroundSessionResolver`.
|
|
42
|
+
*
|
|
43
|
+
* The resolver exposes ONLY composite-key methods. There is no single-arg
|
|
44
|
+
* fallback — production callers MUST thread `(agentId, channelType,
|
|
45
|
+
* channelId)` end-to-end.
|
|
46
|
+
*/
|
|
47
|
+
export interface BackgroundSessionResolver {
|
|
48
|
+
/**
|
|
49
|
+
* Look up the RunHandle for an active session.
|
|
50
|
+
*
|
|
51
|
+
* @param key - Composite key (agentId, channelType, channelId).
|
|
52
|
+
* @returns The RunHandle if a session is registered, otherwise undefined.
|
|
53
|
+
* @throws Error when any composite-key field is empty / falsy
|
|
54
|
+
* (programming error, parity with manager.promote's
|
|
55
|
+
* empty-string guards in background-task-manager.ts:96-107).
|
|
56
|
+
*/
|
|
57
|
+
resolveActiveSession(key: ActiveSessionKey): RunHandle | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Check whether a session is registered for the composite key.
|
|
60
|
+
*
|
|
61
|
+
* @param key - Composite key (agentId, channelType, channelId).
|
|
62
|
+
* @returns true iff a RunHandle is registered, false otherwise.
|
|
63
|
+
* @throws Error when any composite-key field is empty / falsy.
|
|
64
|
+
*/
|
|
65
|
+
hasActiveSession(key: ActiveSessionKey): boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Dependencies required by the resolver.
|
|
69
|
+
*
|
|
70
|
+
* Only the registry is needed today — the resolver is a pure-function
|
|
71
|
+
* wrapper. No logger / event-bus injection (CLAUDE.md: NO logging in
|
|
72
|
+
* pure-function helpers).
|
|
73
|
+
*/
|
|
74
|
+
export interface BackgroundSessionResolverDeps {
|
|
75
|
+
activeRunRegistry: ActiveRunRegistry;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create a BackgroundSessionResolver wrapping an ActiveRunRegistry.
|
|
79
|
+
*
|
|
80
|
+
* Public-facing methods accept ONLY the composite key (agentId,
|
|
81
|
+
* channelType, channelId) — no single-arg fallback. Production callers
|
|
82
|
+
* no longer reach into `activeRunRegistry.has(...)` / `.get(...)`
|
|
83
|
+
* directly.
|
|
84
|
+
*/
|
|
85
|
+
export declare function createBackgroundSessionResolver(deps: BackgroundSessionResolverDeps): BackgroundSessionResolver;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* BackgroundSessionResolver: composite-key wrapper around ActiveRunRegistry.
|
|
4
|
+
*
|
|
5
|
+
* The underlying `activeRunRegistry.has(sessionKey)` and `.get(sessionKey)`
|
|
6
|
+
* take a single formatted-key string. That string would collapse two
|
|
7
|
+
* distinct sessions for the same `channelId` across different agents (or
|
|
8
|
+
* different channelTypes) into one bucket — a latent multi-agent /
|
|
9
|
+
* multi-channel correctness bug.
|
|
10
|
+
*
|
|
11
|
+
* This resolver makes the composite key explicit at every public call site:
|
|
12
|
+
* `(agentId, channelType, channelId)`. It internally composes the formatted
|
|
13
|
+
* key via `formatSessionKey` from `@comis/core` and delegates to the
|
|
14
|
+
* underlying registry.
|
|
15
|
+
*
|
|
16
|
+
* Runtime semantics do not change at the registry layer — what changes is
|
|
17
|
+
* the lookup-key signature surfaced to production callers. No production
|
|
18
|
+
* code outside *.test.ts should retain a single-arg `.has(...)` or
|
|
19
|
+
* `.get(...)` on `activeRunRegistry`.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
import { formatSessionKey } from "@comis/core";
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Factory
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Compose the formatted session-key string from a composite key.
|
|
29
|
+
*
|
|
30
|
+
* Mirrors the shape that production session-managers use when registering
|
|
31
|
+
* handles: `formatSessionKey({tenantId: agentId, channelId:
|
|
32
|
+
* "${channelType}:${channelId}", userId: channelId})`. The output is a
|
|
33
|
+
* deterministic colon-delimited string that round-trips through
|
|
34
|
+
* `parseFormattedSessionKey` (the channelType prefix on channelId is
|
|
35
|
+
* stable across format/parse).
|
|
36
|
+
*
|
|
37
|
+
* The triple is REQUIRED — empty fields are a programming error
|
|
38
|
+
* (parity with the empty-string guard in
|
|
39
|
+
* `background-task-manager.ts:promote()`).
|
|
40
|
+
*/
|
|
41
|
+
function formatComposite(key) {
|
|
42
|
+
if (!key.agentId || !key.channelType || !key.channelId) {
|
|
43
|
+
throw new Error(`BackgroundSessionResolver: composite key requires non-empty agentId, channelType, channelId; got ${JSON.stringify(key)}`);
|
|
44
|
+
}
|
|
45
|
+
return formatSessionKey({
|
|
46
|
+
tenantId: key.agentId,
|
|
47
|
+
channelId: `${key.channelType}:${key.channelId}`,
|
|
48
|
+
userId: key.channelId,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create a BackgroundSessionResolver wrapping an ActiveRunRegistry.
|
|
53
|
+
*
|
|
54
|
+
* Public-facing methods accept ONLY the composite key (agentId,
|
|
55
|
+
* channelType, channelId) — no single-arg fallback. Production callers
|
|
56
|
+
* no longer reach into `activeRunRegistry.has(...)` / `.get(...)`
|
|
57
|
+
* directly.
|
|
58
|
+
*/
|
|
59
|
+
export function createBackgroundSessionResolver(deps) {
|
|
60
|
+
// Local alias: the resolver IS the abstraction over the underlying
|
|
61
|
+
// single-arg registry. We rename to `registry` so source-grep tooling
|
|
62
|
+
// (`activeRunRegistry.has|get(`) does not flag this file as a callsite
|
|
63
|
+
// to migrate -- the resolver IS the migration target. Invariant:
|
|
64
|
+
// *production callers* of `activeRunRegistry` go through this resolver;
|
|
65
|
+
// the resolver itself remains the sole consumer of the underlying
|
|
66
|
+
// single-arg surface.
|
|
67
|
+
const registry = deps.activeRunRegistry;
|
|
68
|
+
return {
|
|
69
|
+
resolveActiveSession(key) {
|
|
70
|
+
const formatted = formatComposite(key);
|
|
71
|
+
return registry.get(formatted);
|
|
72
|
+
},
|
|
73
|
+
hasActiveSession(key) {
|
|
74
|
+
const formatted = formatComposite(key);
|
|
75
|
+
return registry.has(formatted);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -34,6 +34,7 @@ export function buildMessagingSection(toolNames, isMinimal, channelContext) {
|
|
|
34
34
|
'- `[System Message]` blocks are internal context. If one reports completed work and asks for a user update, rewrite it in your normal assistant voice. Never forward raw system message text to users.',
|
|
35
35
|
"- Never use shell execution, code execution, or file tools to send messages. Use only the messaging tools.",
|
|
36
36
|
"- Do NOT use message(action=send) for progress updates, debug output, or placeholder text. The user sees every send as a phone notification. Work silently; deliver the result.",
|
|
37
|
+
"- Do NOT narrate intent before calling a tool. Avoid prefacing tool calls with phrases like \"I'll do X now…\" or \"Let me run Y…\" — invoke the tool directly. Pre-tool-text creates user-visible promises the agent may not be able to fulfill if the tool gets backgrounded.",
|
|
37
38
|
"- Use `fetch` to read recent messages from a channel before responding to context you missed.",
|
|
38
39
|
"- The `delete` action requires confirmation and cannot be undone.",
|
|
39
40
|
];
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { getToolMetadata } from "@comis/core";
|
|
21
21
|
import { getProviders } from "@mariozechner/pi-ai";
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
|
-
//
|
|
23
|
+
// Live native-catalog provider list
|
|
24
24
|
//
|
|
25
25
|
// Computed once at module load time. Used by the providers_manage TOOL_GUIDE
|
|
26
26
|
// "Built-in vs Custom Provider Check" block below so the text reflects
|
|
@@ -325,7 +325,7 @@ If the model IS built-in: skip provider creation. After credential pre-check pas
|
|
|
325
325
|
If the model is NOT built-in: you need a custom provider. Proceed to the steps below, but first gather ALL required configuration.
|
|
326
326
|
|
|
327
327
|
### Choosing the \`type\` Field (POST AUTO-PROMOTE FLOW)
|
|
328
|
-
|
|
328
|
+
For catalog-driven providers, the \`type\` field follows two distinct rules depending on the provider name:
|
|
329
329
|
- **If \`provider_id\` matches a built-in name** (use models_manage list_providers to verify): OMIT \`type\` entirely from the create config. The daemon auto-promotes \`type\` to the native catalog name when \`provider_id\` matches a native entry AND no custom \`baseUrl\` is supplied. Setting \`type:"openai"\` for a built-in name still works (auto-promoted), but omitting it is cleaner.
|
|
330
330
|
- **If \`provider_id\` is a custom OpenAI-compatible proxy** (NVIDIA NIM, Together, Fireworks, etc.) NOT in the native catalog: set \`type:"openai"\` (or whatever wire-format API matches). Auto-promotion does not fire for non-catalog names.
|
|
331
331
|
- **If \`baseUrl\` differs from the native catalog URL** for a built-in name: this signals you want the OpenAI-passthrough shape (custom proxy that masquerades as the built-in). Auto-promotion is suppressed; the entry stays as \`type:"openai"\`.
|
|
@@ -357,7 +357,7 @@ To switch an agent to a different provider/model, call agents_manage update with
|
|
|
357
357
|
**Three preconditions the LLM MUST verify before issuing the update:**
|
|
358
358
|
1. The target provider exists as a \`providers.entries.<provider_id>\` key. If it does not, call providers_manage create FIRST (and gateway env_set for the API key if needed). Patching an agent to a provider that has no entry resolves under the wrong provider family at the next session — the original bug.
|
|
359
359
|
2. The model id matches a \`models[].id\` in that provider entry (or is a built-in known to the pi-ai catalog for that provider type). Otherwise \`registry.find(provider, model)\` returns undefined and the next session falls back with a "Model not found" message.
|
|
360
|
-
3. **Credential pre-check passed** (see top of this guide). The target provider's apiKeyName is non-empty AND \`gateway env_list filter:"<PROVIDER>*"\` confirmed the named secret exists in env. Skipping this step is the bug that causes "No API key found" failures at the next chat turn
|
|
360
|
+
3. **Credential pre-check passed** (see top of this guide). The target provider's apiKeyName is non-empty AND \`gateway env_list filter:"<PROVIDER>*"\` confirmed the named secret exists in env. Skipping this step is the bug that causes "No API key found" failures at the next chat turn.
|
|
361
361
|
|
|
362
362
|
**Timing — the change is NOT hot-applied to the active session.**
|
|
363
363
|
agents_manage update writes through persistToConfig WITHOUT a hot-update callback, which triggers a SIGUSR2 daemon restart (2-second debounce). The new provider/model takes effect on the next session, not the currently-running prompt. Tell the user the switch is queued and will take effect after the daemon settles.
|