comisai 1.0.25 → 1.0.26

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.
Files changed (145) hide show
  1. package/node_modules/@comis/agent/dist/bootstrap/sections/tool-descriptions.js +130 -10
  2. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.d.ts +1 -1
  3. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.js +9 -2
  4. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.d.ts +8 -0
  5. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.js +2 -0
  6. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.d.ts +29 -0
  7. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.js +242 -2
  8. package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.d.ts +210 -0
  9. package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.js +566 -0
  10. package/node_modules/@comis/agent/dist/context-engine/context-engine.js +8 -6
  11. package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.d.ts +51 -30
  12. package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.js +109 -36
  13. package/node_modules/@comis/agent/dist/executor/executor-context-engine-setup.js +5 -1
  14. package/node_modules/@comis/agent/dist/executor/executor-post-execution.js +22 -20
  15. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.d.ts +2 -0
  16. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +111 -15
  17. package/node_modules/@comis/agent/dist/executor/executor-response-filter.d.ts +20 -17
  18. package/node_modules/@comis/agent/dist/executor/executor-response-filter.js +132 -52
  19. package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +16 -3
  20. package/node_modules/@comis/agent/dist/executor/model-retry.d.ts +14 -0
  21. package/node_modules/@comis/agent/dist/executor/model-retry.js +72 -1
  22. package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +3 -0
  23. package/node_modules/@comis/agent/dist/executor/pi-executor.js +68 -9
  24. package/node_modules/@comis/agent/dist/executor/post-batch-continuation.d.ts +82 -0
  25. package/node_modules/@comis/agent/dist/executor/post-batch-continuation.js +200 -0
  26. package/node_modules/@comis/agent/dist/executor/stream-wrappers/request-body-injector.js +1 -9
  27. package/node_modules/@comis/agent/dist/executor/tool-deferral.d.ts +37 -2
  28. package/node_modules/@comis/agent/dist/executor/tool-deferral.js +45 -3
  29. package/node_modules/@comis/agent/dist/executor/tool-parallelism.js +0 -1
  30. package/node_modules/@comis/agent/dist/executor/types.d.ts +11 -2
  31. package/node_modules/@comis/agent/dist/index.d.ts +3 -1
  32. package/node_modules/@comis/agent/dist/index.js +2 -0
  33. package/node_modules/@comis/agent/dist/model/last-known-model.d.ts +36 -0
  34. package/node_modules/@comis/agent/dist/model/last-known-model.js +49 -0
  35. package/node_modules/@comis/agent/dist/model/model-registry-adapter.d.ts +16 -4
  36. package/node_modules/@comis/agent/dist/model/model-registry-adapter.js +65 -21
  37. package/node_modules/@comis/agent/dist/planner/types.d.ts +0 -2
  38. package/node_modules/@comis/agent/dist/session/comis-session-manager.d.ts +10 -0
  39. package/node_modules/@comis/agent/dist/session/comis-session-manager.js +5 -0
  40. package/node_modules/@comis/agent/dist/spawn/pi-mono-adapters.js +7 -0
  41. package/node_modules/@comis/agent/package.json +1 -1
  42. package/node_modules/@comis/channels/package.json +1 -1
  43. package/node_modules/@comis/cli/dist/client/rpc-client.js +6 -1
  44. package/node_modules/@comis/cli/dist/commands/doctor.js +5 -3
  45. package/node_modules/@comis/cli/dist/commands/health.js +5 -2
  46. package/node_modules/@comis/cli/dist/wizard/json-output.js +7 -3
  47. package/node_modules/@comis/cli/dist/wizard/steps/11-daemon-start.js +130 -0
  48. package/node_modules/@comis/cli/package.json +1 -1
  49. package/node_modules/@comis/core/dist/config/immutable-keys.d.ts +2 -2
  50. package/node_modules/@comis/core/dist/config/immutable-keys.js +8 -3
  51. package/node_modules/@comis/core/dist/config/managed-sections.d.ts +43 -4
  52. package/node_modules/@comis/core/dist/config/managed-sections.js +100 -6
  53. package/node_modules/@comis/core/dist/config/schema-agent.d.ts +39 -0
  54. package/node_modules/@comis/core/dist/config/schema-agent.js +14 -0
  55. package/node_modules/@comis/core/dist/config/schema.d.ts +4 -0
  56. package/node_modules/@comis/core/dist/config/schema.js +14 -0
  57. package/node_modules/@comis/core/dist/domain/execution-graph.d.ts +1 -1
  58. package/node_modules/@comis/core/dist/event-bus/events-agent.d.ts +17 -2
  59. package/node_modules/@comis/core/dist/exports/config.d.ts +2 -2
  60. package/node_modules/@comis/core/dist/exports/config.js +1 -1
  61. package/node_modules/@comis/core/package.json +1 -1
  62. package/node_modules/@comis/daemon/dist/daemon.d.ts +22 -0
  63. package/node_modules/@comis/daemon/dist/daemon.js +42 -0
  64. package/node_modules/@comis/daemon/dist/rpc/agent-handlers.d.ts +5 -2
  65. package/node_modules/@comis/daemon/dist/rpc/agent-handlers.js +80 -1
  66. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.d.ts +67 -0
  67. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.js +139 -0
  68. package/node_modules/@comis/daemon/dist/rpc/model-handlers.d.ts +3 -0
  69. package/node_modules/@comis/daemon/dist/rpc/model-handlers.js +29 -5
  70. package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.d.ts +30 -0
  71. package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.js +59 -0
  72. package/node_modules/@comis/daemon/dist/rpc/provider-handlers.d.ts +37 -0
  73. package/node_modules/@comis/daemon/dist/rpc/provider-handlers.js +330 -0
  74. package/node_modules/@comis/daemon/dist/rpc/rpc-dispatch.js +18 -1
  75. package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.d.ts +4 -0
  76. package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.js +30 -0
  77. package/node_modules/@comis/daemon/dist/wiring/setup-agents.d.ts +3 -1
  78. package/node_modules/@comis/daemon/dist/wiring/setup-agents.js +28 -2
  79. package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.js +1 -0
  80. package/node_modules/@comis/daemon/dist/wiring/setup-tools.js +7 -4
  81. package/node_modules/@comis/daemon/package.json +1 -1
  82. package/node_modules/@comis/gateway/package.json +1 -1
  83. package/node_modules/@comis/infra/dist/index.d.ts +1 -0
  84. package/node_modules/@comis/infra/dist/index.js +2 -0
  85. package/node_modules/@comis/infra/dist/runtime/is-docker.d.ts +1 -0
  86. package/node_modules/@comis/infra/dist/runtime/is-docker.js +25 -0
  87. package/node_modules/@comis/infra/package.json +1 -1
  88. package/node_modules/@comis/memory/package.json +1 -1
  89. package/node_modules/@comis/scheduler/package.json +1 -1
  90. package/node_modules/@comis/shared/package.json +1 -1
  91. package/node_modules/@comis/skills/dist/bridge/tool-metadata-registry.js +1 -3
  92. package/node_modules/@comis/skills/dist/builtin/platform/admin-manage-factory.js +24 -1
  93. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.d.ts +53 -7
  94. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.js +218 -24
  95. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.d.ts +4 -1
  96. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.js +16 -1
  97. package/node_modules/@comis/skills/dist/builtin/platform/index.d.ts +1 -1
  98. package/node_modules/@comis/skills/dist/builtin/platform/index.js +1 -1
  99. package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.d.ts +56 -0
  100. package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.js +203 -0
  101. package/node_modules/@comis/skills/dist/index.d.ts +1 -1
  102. package/node_modules/@comis/skills/dist/index.js +2 -2
  103. package/node_modules/@comis/skills/dist/policy/tool-policy.js +0 -1
  104. package/node_modules/@comis/skills/package.json +1 -1
  105. package/node_modules/@comis/web/dist/assets/{agent-detail-ru-AhppM.js → agent-detail-DqL6Artv.js} +1 -1
  106. package/node_modules/@comis/web/dist/assets/{agent-editor-hjwRuFVp.js → agent-editor-CNM_h94Y.js} +1 -1
  107. package/node_modules/@comis/web/dist/assets/{agent-list-6Uotjatr.js → agent-list-Dbh-xD_F.js} +1 -1
  108. package/node_modules/@comis/web/dist/assets/{billing-view-CxysXH0p.js → billing-view-C1DmtyzK.js} +1 -1
  109. package/node_modules/@comis/web/dist/assets/{channel-detail-BBCKtmne.js → channel-detail-CtCH22N1.js} +1 -1
  110. package/node_modules/@comis/web/dist/assets/{channel-list-FkfeOLBQ.js → channel-list-C7xXn-60.js} +1 -1
  111. package/node_modules/@comis/web/dist/assets/{chat-console-BumBaIgO.js → chat-console-C51pjFwk.js} +1 -1
  112. package/node_modules/@comis/web/dist/assets/{config-editor-C9BSwHGy.js → config-editor-BLArYRB7.js} +1 -1
  113. package/node_modules/@comis/web/dist/assets/{context-dag-browser-BHm00mJD.js → context-dag-browser-fuyMinNI.js} +1 -1
  114. package/node_modules/@comis/web/dist/assets/{context-engine-BENY3pWE.js → context-engine-Bngf2bH0.js} +1 -1
  115. package/node_modules/@comis/web/dist/assets/{delivery-view-BCnkPsAp.js → delivery-view-C80hucxX.js} +1 -1
  116. package/node_modules/@comis/web/dist/assets/{diagnostics-view-C_jQFG2H.js → diagnostics-view-Cl4VbHZ6.js} +1 -1
  117. package/node_modules/@comis/web/dist/assets/{ic-chat-message-FdQcZsSQ.js → ic-chat-message-ByFUoMm6.js} +1 -1
  118. package/node_modules/@comis/web/dist/assets/{ic-connection-dot-BgYiK2N4.js → ic-connection-dot-C4nDHgY2.js} +1 -1
  119. package/node_modules/@comis/web/dist/assets/{ic-tool-call-DMPHsLyx.js → ic-tool-call-Bh5kq-yY.js} +1 -1
  120. package/node_modules/@comis/web/dist/assets/{index-FLPhHz8p.js → index-BBkuC-EU.js} +2 -2
  121. package/node_modules/@comis/web/dist/assets/{mcp-management-5jyScQis.js → mcp-management-DB-phOo7.js} +1 -1
  122. package/node_modules/@comis/web/dist/assets/{media-config-J9oT9PPs.js → media-config-CRqZ1ZUH.js} +1 -1
  123. package/node_modules/@comis/web/dist/assets/{media-test-DGTCtM8-.js → media-test-C9vE20Oy.js} +1 -1
  124. package/node_modules/@comis/web/dist/assets/{memory-inspector-D5Re9ptG.js → memory-inspector-CeqfnxMZ.js} +1 -1
  125. package/node_modules/@comis/web/dist/assets/{message-center-cRLK6ZmG.js → message-center-Daup7Mof.js} +1 -1
  126. package/node_modules/@comis/web/dist/assets/{models-D5vu07MR.js → models-DLYnEU8E.js} +1 -1
  127. package/node_modules/@comis/web/dist/assets/{observe-view-CalNNEmd.js → observe-view-BTSt_PO5.js} +1 -1
  128. package/node_modules/@comis/web/dist/assets/{pipeline-builder-DUYDGwZf.js → pipeline-builder-DknfzyLt.js} +1 -1
  129. package/node_modules/@comis/web/dist/assets/{pipeline-history-BAO8brOe.js → pipeline-history-JnHZdeU_.js} +1 -1
  130. package/node_modules/@comis/web/dist/assets/{pipeline-history-detail-DectIoQt.js → pipeline-history-detail-Dg4knsEb.js} +1 -1
  131. package/node_modules/@comis/web/dist/assets/{pipeline-list-BHlaBKww.js → pipeline-list-AEnibjsp.js} +1 -1
  132. package/node_modules/@comis/web/dist/assets/{pipeline-monitor-BhtpNEHf.js → pipeline-monitor-DG7RbIOO.js} +1 -1
  133. package/node_modules/@comis/web/dist/assets/{scheduler-VafN_8xi.js → scheduler-uL1fYKAT.js} +1 -1
  134. package/node_modules/@comis/web/dist/assets/{security-QQXMRTlo.js → security-C3DywRLH.js} +1 -1
  135. package/node_modules/@comis/web/dist/assets/{session-detail-BpZ_8Yih.js → session-detail-BtqCNWXV.js} +1 -1
  136. package/node_modules/@comis/web/dist/assets/{session-list-DfCm8Cec.js → session-list-CJXWa2XT.js} +1 -1
  137. package/node_modules/@comis/web/dist/assets/{setup-wizard-C-z477CG.js → setup-wizard-ywn7oJvu.js} +1 -1
  138. package/node_modules/@comis/web/dist/assets/{skills-BCOGPf6s.js → skills-DX0KYnWD.js} +1 -1
  139. package/node_modules/@comis/web/dist/assets/{subagents-l-auUraL.js → subagents-B8p5YJEB.js} +1 -1
  140. package/node_modules/@comis/web/dist/assets/{workspace-manager-DlvBixiq.js → workspace-manager-CgzNIrw1.js} +1 -1
  141. package/node_modules/@comis/web/dist/index.html +1 -1
  142. package/node_modules/@comis/web/package.json +1 -1
  143. package/package.json +13 -13
  144. package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.d.ts +0 -19
  145. package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.js +0 -39
@@ -10,6 +10,7 @@
10
10
  * @module
11
11
  */
12
12
  import { ModelRegistry } from "@mariozechner/pi-coding-agent";
13
+ import { getModels, getProviders } from "@mariozechner/pi-ai";
13
14
  /**
14
15
  * Create a ModelRegistry from an AuthStorage instance.
15
16
  *
@@ -27,6 +28,18 @@ export function createModelRegistryAdapter(authStorage) {
27
28
  * proxies (NVIDIA NIM, Together, ollama, lm-studio, etc.) work without
28
29
  * code changes.
29
30
  */
31
+ /**
32
+ * Provider types that can register without an API key.
33
+ *
34
+ * Ollama (and similar local inference servers) do not require authentication
35
+ * by default. When a provider entry has a type in this set and no apiKeyName
36
+ * is configured (or the named secret is missing), registration proceeds with
37
+ * the "ollama-no-auth" sentinel instead of being skipped.
38
+ *
39
+ * The sentinel reaches the wire as `Authorization: Bearer ollama-no-auth`.
40
+ * Ollama ignores Authorization unless `OLLAMA_API_KEY` is set server-side.
41
+ */
42
+ const KEYLESS_PROVIDER_TYPES = new Set(["ollama"]);
30
43
  const PROVIDER_TYPE_TO_API = {
31
44
  openai: "openai-completions",
32
45
  groq: "openai-completions",
@@ -39,6 +52,18 @@ const PROVIDER_TYPE_TO_API = {
39
52
  anthropic: "anthropic-messages",
40
53
  google: "google-generative-ai",
41
54
  };
55
+ const _builtInProviders = new Set(getProviders());
56
+ function getBuiltInBaseUrl(type) {
57
+ if (!_builtInProviders.has(type))
58
+ return undefined;
59
+ const models = getModels(type);
60
+ return models[0]?.baseUrl;
61
+ }
62
+ function getBuiltInModelIds(type) {
63
+ if (!_builtInProviders.has(type))
64
+ return new Set();
65
+ return new Set(getModels(type).map((m) => m.id));
66
+ }
42
67
  /**
43
68
  * Register YAML `providers.entries.*` with pi-coding-agent's ModelRegistry.
44
69
  *
@@ -50,50 +75,66 @@ const PROVIDER_TYPE_TO_API = {
50
75
  * Per-entry behavior:
51
76
  * - Skipped if `enabled === false`.
52
77
  * - Skipped if no models declared and no `baseUrl` override.
78
+ * - Models that already exist in the built-in pi SDK catalog for the
79
+ * entry's `type` are filtered out (no redundant registration).
53
80
  * - On `registerProvider` error (missing baseUrl, missing apiKey, etc.),
54
81
  * a WARN is logged and the loop continues -- one bad entry must not
55
82
  * prevent the daemon from starting.
56
- *
57
- * @returns Number of entries successfully registered.
58
83
  */
59
84
  export function registerCustomProviders(registry, entries, secretManager, logger) {
60
85
  let registered = 0;
86
+ const providerAliases = new Map();
61
87
  for (const [providerName, entry] of Object.entries(entries)) {
62
88
  if (!entry.enabled) {
63
89
  logger.debug({ providerName }, "Custom provider skipped (disabled)");
64
90
  continue;
65
91
  }
66
- const hasModels = entry.models.length > 0;
92
+ const builtInIds = getBuiltInModelIds(entry.type);
93
+ const isBuiltInType = builtInIds.size > 0;
94
+ if (isBuiltInType && providerName !== entry.type) {
95
+ providerAliases.set(providerName, entry.type);
96
+ }
97
+ const customModels = isBuiltInType
98
+ ? entry.models.filter((m) => !builtInIds.has(m.id))
99
+ : [...entry.models];
100
+ if (isBuiltInType && customModels.length < entry.models.length) {
101
+ const skipped = entry.models.length - customModels.length;
102
+ logger.debug({ providerName, type: entry.type, skipped, remaining: customModels.length }, "Skipped built-in models already in pi SDK catalog");
103
+ }
104
+ const hasModels = customModels.length > 0;
67
105
  const hasBaseUrlOverride = !!entry.baseUrl;
68
106
  if (!hasModels && !hasBaseUrlOverride) {
69
- logger.debug({ providerName }, "Custom provider skipped (no models and no baseUrl override)");
107
+ logger.debug({ providerName }, "Custom provider skipped (no custom models and no baseUrl override)");
70
108
  continue;
71
109
  }
72
110
  const apiKey = entry.apiKeyName ? secretManager.get(entry.apiKeyName) : undefined;
73
- if (hasModels && !apiKey) {
111
+ const isKeylessType = KEYLESS_PROVIDER_TYPES.has(entry.type);
112
+ if (hasModels && !apiKey && !isKeylessType) {
74
113
  logger.warn({
75
114
  providerName,
76
115
  apiKeyName: entry.apiKeyName,
77
- hint: "Set the named secret in ~/.comis/.env or remove the provider entry from config.yaml",
116
+ hint: "Set the named secret in ~/.comis/.env, omit apiKeyName for type='ollama', or remove the provider entry from config.yaml",
78
117
  errorKind: "config",
79
118
  }, "Custom provider has models but no API key -- skipping registration");
80
119
  continue;
81
120
  }
82
121
  const api = PROVIDER_TYPE_TO_API[entry.type] ?? "openai-completions";
83
122
  const headersResolved = Object.keys(entry.headers).length > 0 ? entry.headers : undefined;
123
+ const resolvedApiKey = apiKey ?? (isKeylessType ? "ollama-no-auth" : undefined);
124
+ if (resolvedApiKey === "ollama-no-auth") {
125
+ logger.debug({
126
+ providerName,
127
+ hint: "Using keyless sentinel for type='ollama'. If your Ollama server requires an OLLAMA_API_KEY, set the provider's apiKeyName explicitly via providers_manage update.",
128
+ }, "Custom provider registered with keyless sentinel");
129
+ }
84
130
  try {
85
131
  registry.registerProvider(providerName, {
86
132
  api,
87
- baseUrl: entry.baseUrl || undefined,
88
- apiKey,
133
+ baseUrl: entry.baseUrl || getBuiltInBaseUrl(entry.type),
134
+ apiKey: resolvedApiKey,
89
135
  headers: headersResolved,
90
- // pi's ProviderModelConfig requires concrete values for name/cost/
91
- // contextWindow/maxTokens. Comis's UserModelSchema lets users omit
92
- // these (defaults to optional/undefined), so we fill in zeros and
93
- // a generous default context window. Cost is informational only
94
- // and our CostTracker uses pi-ai's own catalog where it can.
95
136
  models: hasModels
96
- ? entry.models.map((m) => ({
137
+ ? customModels.map((m) => ({
97
138
  id: m.id,
98
139
  name: m.name ?? m.id,
99
140
  contextWindow: m.contextWindow ?? 128_000,
@@ -114,7 +155,7 @@ export function registerCustomProviders(registry, entries, secretManager, logger
114
155
  providerName,
115
156
  api,
116
157
  baseUrl: entry.baseUrl,
117
- modelCount: entry.models.length,
158
+ modelCount: customModels.length,
118
159
  }, "Custom provider registered with pi ModelRegistry");
119
160
  }
120
161
  catch (error) {
@@ -126,7 +167,7 @@ export function registerCustomProviders(registry, entries, secretManager, logger
126
167
  }, "Custom provider registration failed");
127
168
  }
128
169
  }
129
- return registered;
170
+ return { registered, providerAliases };
130
171
  }
131
172
  /**
132
173
  * Resolve the initial model for an agent session.
@@ -139,11 +180,15 @@ export function registerCustomProviders(registry, entries, secretManager, logger
139
180
  * @param config - Agent model configuration (provider + model ID)
140
181
  * @param allowlist - Optional model allowlist for enforcement
141
182
  */
142
- export async function resolveInitialModel(registry, config, allowlist) {
143
- // Try to find the exact model in the registry
144
- const model = registry.find(config.provider, config.model);
183
+ export async function resolveInitialModel(registry, config, allowlist, providerAliases) {
184
+ let model = registry.find(config.provider, config.model);
185
+ if (!model && providerAliases) {
186
+ const builtInName = providerAliases.get(config.provider);
187
+ if (builtInName) {
188
+ model = registry.find(builtInName, config.model);
189
+ }
190
+ }
145
191
  if (!model) {
146
- // Try to find any available model from the requested provider
147
192
  const available = registry.getAvailable();
148
193
  const providerModel = available.find((m) => m.provider === config.provider);
149
194
  if (!providerModel) {
@@ -154,7 +199,6 @@ export async function resolveInitialModel(registry, config, allowlist) {
154
199
  `Available models: ${available.map((m) => `${m.provider}/${m.id}`).join(", ") || "none"}`,
155
200
  };
156
201
  }
157
- // Found a provider model but not the exact ID -- return it with a note
158
202
  return {
159
203
  model: undefined,
160
204
  thinkingLevel: "off",
@@ -31,8 +31,6 @@ export interface ExecutionPlan {
31
31
  steps: PlanStep[];
32
32
  /** Number of steps marked "done". */
33
33
  completedCount: number;
34
- /** Whether the completeness nudge has been injected. */
35
- nudged: boolean;
36
34
  /** Timestamp of plan creation. */
37
35
  createdAtMs: number;
38
36
  }
@@ -104,6 +104,16 @@ export interface ComisSessionManager {
104
104
  * Fire-and-forget -- metadata write failure must not affect execution.
105
105
  */
106
106
  writeSessionMetadata(sessionKey: SessionKey, metadata: SessionMetadata): void;
107
+ /**
108
+ * 260428-iag: Resolve the absolute JSONL session file path for a session key.
109
+ *
110
+ * Thin synchronous wrapper around `sessionKeyToPath(sessionKey, deps.sessionBaseDir)`
111
+ * that exposes the path resolver to the wire-edge diagnostic in pi-event-bridge.
112
+ * Pure delegation -- no I/O, no logging, no side effects. Path composition is
113
+ * delegated to `sessionKeyToPath`, which uses `safePath` for traversal-safe
114
+ * resolution.
115
+ */
116
+ getSessionPath(sessionKey: SessionKey): string;
107
117
  }
108
118
  /**
109
119
  * Create a ComisSessionManager that manages session lifecycle with write locks.
@@ -70,6 +70,11 @@ export function createComisSessionManager(deps) {
70
70
  catch { /* non-empty or already gone */ }
71
71
  }, { retries: 10, retryMinTimeout: 500 });
72
72
  },
73
+ getSessionPath(sessionKey) {
74
+ // 260428-iag wire-edge diagnostic: pure delegation to sessionKeyToPath
75
+ // (which uses safePath internally). No I/O, no logging.
76
+ return sessionKeyToPath(sessionKey, deps.sessionBaseDir);
77
+ },
73
78
  writeSessionMetadata(sessionKey, metadata) {
74
79
  const sessionPath = sessionKeyToPath(sessionKey, deps.sessionBaseDir);
75
80
  const metadataPath = sessionPath.replace(/\.jsonl$/, "_session-metadata.json");
@@ -50,5 +50,12 @@ export function createEphemeralComisSessionManager(cwd) {
50
50
  writeSessionMetadata() {
51
51
  // No-op: no companion file for in-memory sessions
52
52
  },
53
+ getSessionPath() {
54
+ // 260428-iag wire-edge diagnostic: ephemeral sub-agent sessions never
55
+ // persist a JSONL file, so there is no path to return. Empty string
56
+ // signals "no persisted file"; the bridge's wire-diff hook short-circuits
57
+ // when jsonlPath.length === 0.
58
+ return "";
59
+ },
53
60
  };
54
61
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/agent",
3
3
  "private": true,
4
- "version": "1.0.25",
4
+ "version": "1.0.26",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "AI agent executor, budget control, and session management for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/channels",
3
3
  "private": true,
4
- "version": "1.0.25",
4
+ "version": "1.0.26",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Chat platform adapters — Discord, Telegram, Slack, WhatsApp, Signal, iMessage, IRC, LINE",
@@ -127,8 +127,13 @@ function resolveFromConfig() {
127
127
  token = undefined;
128
128
  }
129
129
  }
130
+ // gateway.host is a *bind* address. As a *connect* host, the wildcard
131
+ // values (0.0.0.0 / ::) aren't valid — remap to loopback so the CLI
132
+ // can reach a daemon that's binding all interfaces (the default for
133
+ // LAN / Docker deployments).
134
+ const connectHost = host === "0.0.0.0" ? "127.0.0.1" : host === "::" ? "::1" : host;
130
135
  const protocol = tls ? "wss" : "ws";
131
- return { url: `${protocol}://${host}:${port}/ws`, token, tls };
136
+ return { url: `${protocol}://${connectHost}:${port}/ws`, token, tls };
132
137
  }
133
138
  catch {
134
139
  return { url: FALLBACK_GATEWAY_URL, token: undefined, tls: false };
@@ -76,12 +76,14 @@ function buildDoctorContext(configPaths) {
76
76
  }
77
77
  const dataDir = config?.dataDir || os.homedir() + "/.comis";
78
78
  const daemonPidFile = dataDir + "/daemon.pid";
79
- // Resolve gateway URL from config
79
+ // Resolve gateway URL from config. gw.host is a *bind* address; remap
80
+ // wildcards to loopback so the connectivity probe targets a real address.
80
81
  let gatewayUrl;
81
82
  if (config?.gateway) {
82
83
  const gw = config.gateway;
83
- const host = gw.host || "127.0.0.1";
84
- const port = gw.port || 3000;
84
+ const bindHost = gw.host || "127.0.0.1";
85
+ const host = bindHost === "0.0.0.0" ? "127.0.0.1" : bindHost === "::" ? "::1" : bindHost;
86
+ const port = gw.port || 4766;
85
87
  const protocol = gw.tls ? "https" : "http";
86
88
  gatewayUrl = `${protocol}://${host}:${port}`;
87
89
  }
@@ -75,11 +75,14 @@ function buildHealthContext(configPaths) {
75
75
  }
76
76
  const dataDir = config?.dataDir || os.homedir() + "/.comis";
77
77
  const daemonPidFile = dataDir + "/daemon.pid";
78
+ // gw.host is a *bind* address; remap wildcards to loopback so the
79
+ // connectivity probe targets a real address.
78
80
  let gatewayUrl;
79
81
  if (config?.gateway) {
80
82
  const gw = config.gateway;
81
- const host = gw.host || "127.0.0.1";
82
- const port = gw.port || 3000;
83
+ const bindHost = gw.host || "127.0.0.1";
84
+ const host = bindHost === "0.0.0.0" ? "127.0.0.1" : bindHost === "::" ? "::1" : bindHost;
85
+ const port = gw.port || 4766;
83
86
  const protocol = gw.tls ? "https" : "http";
84
87
  gatewayUrl = `${protocol}://${host}:${port}`;
85
88
  }
@@ -34,13 +34,17 @@ export function buildJsonOutput(state, opts) {
34
34
  let gatewayUrl;
35
35
  let gatewayToken;
36
36
  if (state.gateway) {
37
- const bindIp = state.gateway.bindMode === "loopback"
37
+ // For LAN mode the daemon binds 0.0.0.0, but the printed URL is
38
+ // a *connect* hint — surface 127.0.0.1 (the only address every
39
+ // local caller can reach). External clients use the host's public
40
+ // IP/hostname; surfacing 0.0.0.0 here would just be misleading.
41
+ const connectIp = state.gateway.bindMode === "loopback"
38
42
  ? "127.0.0.1"
39
43
  : state.gateway.bindMode === "lan"
40
- ? "0.0.0.0"
44
+ ? "127.0.0.1"
41
45
  : state.gateway.customIp ?? "127.0.0.1";
42
46
  const port = state.gateway.port ?? 4766;
43
- gatewayUrl = `ws://${bindIp}:${port}`;
47
+ gatewayUrl = `ws://${connectIp}:${port}`;
44
48
  // Only include token when auth method is token
45
49
  if (state.gateway.authMethod === "token" && state.gateway.token) {
46
50
  gatewayToken = state.gateway.token;
@@ -18,6 +18,7 @@ import { existsSync, mkdirSync, writeFileSync, openSync, closeSync, accessSync,
18
18
  import * as os from "node:os";
19
19
  import { promisify } from "node:util";
20
20
  import { safePath } from "@comis/core";
21
+ import { isDocker } from "@comis/infra";
21
22
  const exec = promisify(execFile);
22
23
  import { updateState, sectionSeparator, success as themeSuccess, error as themeError, } from "../index.js";
23
24
  // ---------- Constants ----------
@@ -76,6 +77,45 @@ async function waitForReady(host, port) {
76
77
  }
77
78
  return false;
78
79
  }
80
+ /**
81
+ * Poll the gateway through a stop/start cycle.
82
+ *
83
+ * Used after signalling the in-container daemon for a Docker-managed
84
+ * restart: first waits for the gateway to go down (proving the signal
85
+ * landed and the container is exiting), then for a fresh gateway to
86
+ * come back (proving Docker's restart policy respawned the container).
87
+ * Returns false if either phase times out.
88
+ */
89
+ async function waitForRestart(host, port) {
90
+ const url = `http://${host}:${port}/health`;
91
+ const probe = async () => {
92
+ try {
93
+ const controller = new AbortController();
94
+ const timer = setTimeout(() => controller.abort(), 1500);
95
+ const res = await fetch(url, { signal: controller.signal });
96
+ clearTimeout(timer);
97
+ return res.ok;
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ };
103
+ // Phase 1: wait for the gateway to disappear (signal landed).
104
+ const downDeadline = Date.now() + 5_000;
105
+ while (Date.now() < downDeadline) {
106
+ if (!(await probe()))
107
+ break;
108
+ await new Promise((r) => setTimeout(r, READY_POLL_MS));
109
+ }
110
+ // Phase 2: wait for the gateway to come back (container restarted).
111
+ const upDeadline = Date.now() + READY_TIMEOUT_MS;
112
+ while (Date.now() < upDeadline) {
113
+ if (await probe())
114
+ return true;
115
+ await new Promise((r) => setTimeout(r, READY_POLL_MS));
116
+ }
117
+ return false;
118
+ }
79
119
  /**
80
120
  * Run subsystem health checks and display results.
81
121
  *
@@ -196,6 +236,42 @@ async function runHealthCheck(state, prompter, gatewayHost, gatewayPort) {
196
236
  }
197
237
  }
198
238
  }
239
+ /**
240
+ * Find the comis daemon PID inside the container.
241
+ *
242
+ * Scans /proc for processes whose parent is PID 1 and whose cmdline
243
+ * contains "daemon.js". Used to signal the container's daemon for a
244
+ * Docker-native restart instead of spawning a sibling.
245
+ */
246
+ async function findContainerDaemonPid() {
247
+ try {
248
+ const { readdirSync, readFileSync } = await import("node:fs");
249
+ for (const entry of readdirSync("/proc")) {
250
+ if (!/^\d+$/.test(entry))
251
+ continue;
252
+ const pid = Number(entry);
253
+ if (pid === 1 || pid === process.pid)
254
+ continue;
255
+ let cmdline;
256
+ let ppid;
257
+ try {
258
+ cmdline = readFileSync(`/proc/${entry}/cmdline`, "utf-8");
259
+ const status = readFileSync(`/proc/${entry}/status`, "utf-8");
260
+ ppid = (/^PPid:\s*(\d+)/m.exec(status)?.[1]) ?? "";
261
+ }
262
+ catch {
263
+ continue;
264
+ }
265
+ if (ppid !== "1")
266
+ continue;
267
+ if (!cmdline.includes("daemon.js"))
268
+ continue;
269
+ return pid;
270
+ }
271
+ }
272
+ catch { /* /proc not accessible */ }
273
+ return undefined;
274
+ }
199
275
  async function detectServiceManager() {
200
276
  if (!existsSync("/run/systemd/system"))
201
277
  return "direct";
@@ -351,6 +427,60 @@ export const daemonStartStep = {
351
427
  return updateState(state, {});
352
428
  }
353
429
  // 4. Direct-spawn fallback (no systemd)
430
+ // Docker branch: the daemon is the container's PID 1 process tree.
431
+ // Spawning a sibling produces EADDRINUSE; killing PID 1 directly via
432
+ // pid-file (which the container daemon doesn't write) is a no-op.
433
+ // Signal the actual daemon process so dumb-init exits and Docker's
434
+ // restart policy brings the container back with the new config.
435
+ if (isDocker()) {
436
+ const dockerSpinner = prompter.spinner();
437
+ if (choice === "restart" || daemonRunning) {
438
+ dockerSpinner.start("Signalling container daemon to restart...");
439
+ const targetPid = await findContainerDaemonPid();
440
+ if (targetPid) {
441
+ // 260428-qrn: Prime the user BEFORE the SIGTERM so they have a
442
+ // breadcrumb pointing at the missing `--restart unless-stopped`
443
+ // flag. This is a heads-up, not an abort -- the existing
444
+ // waitForRestart detector still handles the failure path below.
445
+ prompter.log.warn("Restarting daemon via SIGTERM. The container must have been started with `--restart unless-stopped` (or equivalent compose `restart: unless-stopped`). If the container exits and stays exited, run `docker restart <container-name>` from your host.");
446
+ try {
447
+ process.kill(targetPid, "SIGTERM");
448
+ }
449
+ catch { /* already gone */ }
450
+ // Wait for the gateway to disappear, then for it to come back
451
+ // (Docker restart policy respawns the container).
452
+ const restarted = await waitForRestart(host, port);
453
+ if (restarted) {
454
+ dockerSpinner.stop("Daemon restarted via container restart policy");
455
+ }
456
+ else {
457
+ dockerSpinner.stop("Daemon stopped, but the container did not auto-restart");
458
+ prompter.log.warn("Run `docker restart <container>` (or `docker start <container>` if it exited) to apply the new config.");
459
+ return updateState(state, {});
460
+ }
461
+ }
462
+ else {
463
+ dockerSpinner.stop("Could not find the container daemon process");
464
+ prompter.log.warn("Run `docker restart <container>` to apply the new configuration.");
465
+ return updateState(state, {});
466
+ }
467
+ }
468
+ else {
469
+ // Daemon wasn't running and we're inside a container — Docker
470
+ // launches the daemon itself; nothing for the wizard to do here.
471
+ dockerSpinner.start("Waiting for container daemon...");
472
+ const ready = await waitForReady(host, port);
473
+ dockerSpinner.stop(ready ? "Daemon ready" : "Daemon not yet responding");
474
+ if (!ready) {
475
+ prompter.log.warn("Start the container with `docker start <container>` if it isn't already running.");
476
+ return updateState(state, {});
477
+ }
478
+ }
479
+ if (!state.skipHealth) {
480
+ await runHealthCheck(state, prompter, host, port);
481
+ }
482
+ return updateState(state, {});
483
+ }
354
484
  // Stop existing daemon before restart
355
485
  if (choice === "restart") {
356
486
  const stopSpinner = prompter.spinner();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/cli",
3
3
  "private": true,
4
- "version": "1.0.25",
4
+ "version": "1.0.26",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Command-line interface for the Comis AI agent platform",
@@ -25,8 +25,8 @@ export declare const MUTABLE_CONFIG_OVERRIDES: readonly string[];
25
25
  * path segments, the path is considered a match (equal to or a child of the
26
26
  * pattern).
27
27
  *
28
- * @param fullPath - Full dot-notation config path (e.g., "agents.default.persona.name")
29
- * @param pattern - Override pattern with `*` wildcards (e.g., "agents.*.persona")
28
+ * @param fullPath - Full dot-notation config path (e.g., "agents.default.maxSteps")
29
+ * @param pattern - Override pattern with `*` wildcards (e.g., "agents.*.maxSteps")
30
30
  * @returns true if the path matches or is a child of the pattern
31
31
  */
32
32
  export declare function matchesOverridePattern(fullPath: string, pattern: string): boolean;
@@ -22,7 +22,12 @@ export const MUTABLE_CONFIG_OVERRIDES = [
22
22
  "agents.*.skills.watchDebounceMs",
23
23
  "agents.*.skills.discoveryPaths",
24
24
  "agents.*.maxSteps",
25
- "agents.*.persona",
25
+ // 260428-rrr Bug A: removed dead "agents.*.persona" entry. PerAgentConfigSchema
26
+ // is z.strictObject and has no `persona` field, so the override could never
27
+ // produce a successful patch -- it only leaked a misleading capability hint
28
+ // to LLMs (formatRedirectHint emitted "you can also patch agents.<id>.persona")
29
+ // which the LLM echoed back as `persona:` in agents_manage.create config,
30
+ // triggering Zod unrecognized_keys rejection.
26
31
  "agents.*.promptTimeout.promptTimeoutMs", // Allow runtime tuning
27
32
  "agents.*.promptTimeout.retryPromptTimeoutMs", // Allow runtime tuning
28
33
  "agents.*.operationModels", // Allow runtime model tiering tuning
@@ -39,8 +44,8 @@ export const MUTABLE_CONFIG_OVERRIDES = [
39
44
  * path segments, the path is considered a match (equal to or a child of the
40
45
  * pattern).
41
46
  *
42
- * @param fullPath - Full dot-notation config path (e.g., "agents.default.persona.name")
43
- * @param pattern - Override pattern with `*` wildcards (e.g., "agents.*.persona")
47
+ * @param fullPath - Full dot-notation config path (e.g., "agents.default.maxSteps")
48
+ * @param pattern - Override pattern with `*` wildcards (e.g., "agents.*.maxSteps")
44
49
  * @returns true if the path matches or is a child of the pattern
45
50
  */
46
51
  export function matchesOverridePattern(fullPath, pattern) {
@@ -35,6 +35,29 @@ export interface ManagedSectionRedirect {
35
35
  * already present in config.
36
36
  */
37
37
  fullyManaged: boolean;
38
+ /**
39
+ * Compact schema fragment so the LLM can call the tool without a separate
40
+ * discover_tools round-trip. Populated when the action enum + required
41
+ * fields fit in < 20 lines of hint text. Verified against the tool's
42
+ * TypeBox parameter schema as of this commit.
43
+ *
44
+ * Bug B (260428-gj6): production trace c7b91328 showed the agent burning
45
+ * ~30s × 4 LLM calls re-loading the agents_manage schema after an
46
+ * immutable-path rejection. Surfacing the fragment inline closes that
47
+ * round-trip tax.
48
+ */
49
+ schemaFragment?: {
50
+ /** Valid `action` enum values (pinned to the tool's TypeBox Union literals). */
51
+ actions: readonly string[];
52
+ /**
53
+ * Required field names per action -- only entries that are strictly
54
+ * required by the tool's handler (omitting Type.Optional fields with
55
+ * sensible defaults). Omit the whole property when no action has
56
+ * required-beyond-action fields (e.g., channels_manage operates on
57
+ * existing entries only).
58
+ */
59
+ requiredByAction?: Record<string, readonly string[]>;
60
+ };
38
61
  }
39
62
  /**
40
63
  * Registered managed sections.
@@ -55,10 +78,26 @@ export declare function getManagedSectionRedirect(section: string | undefined, k
55
78
  /**
56
79
  * Format an LLM-readable hint for an immutability rejection.
57
80
  *
58
- * The output uses an explicit two-step "Recovery:" framing because smaller
59
- * models (Haiku 4.5, Gemini Flash, GPT-OSS-20b) parse numbered steps more
60
- * reliably than prose. The example call is JSON-stringified compactly so it
61
- * can be copy-pasted into the next tool invocation.
81
+ * Output is a single-step "Recovery: call <tool>(<example>)." line: the
82
+ * dedicated `*_manage` tool auto-loads on first direct invocation under
83
+ * every supported provider path:
84
+ *
85
+ * - Anthropic Sonnet/Opus 4.x: request-body-injector strips client-side
86
+ * `discover_tools` from the payload and marks deferred tools
87
+ * `defer_loading: true`; calling the tool by name auto-loads it.
88
+ * - Anthropic Haiku / OpenAI / xAI / Google: tools surface via the
89
+ * client-side `discover_tools` corpus, but a stub-filter wraps deferred
90
+ * entries so that calling the tool by name still works first try (the
91
+ * stub forwards to the real tool and registers it as discovered).
92
+ *
93
+ * Naming `discover_tools` in the hint actively misleads Anthropic
94
+ * Sonnet/Opus 4.x because that tool is not in their payload (260428-oyc
95
+ * production repro: agent saw "Recovery: (1) call discover_tools(...)" and
96
+ * gave up, reporting "I don't have a discover_tools function"). The
97
+ * single-step framing works on every provider.
98
+ *
99
+ * The example call is JSON-stringified compactly so it can be copy-pasted
100
+ * verbatim into the next tool invocation.
62
101
  *
63
102
  * @param redirect - The matched managed-section entry
64
103
  * @param mutablePaths - Optional override paths for in-place patching of