mixdog 0.9.52 → 0.9.53

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 (111) hide show
  1. package/package.json +1 -1
  2. package/scripts/bench-run.mjs +2 -2
  3. package/scripts/compact-pressure-test.mjs +104 -0
  4. package/scripts/desktop-session-bridge-test.mjs +704 -0
  5. package/scripts/freevar-smoke.mjs +7 -4
  6. package/scripts/lifecycle-api-test.mjs +65 -4
  7. package/scripts/max-output-recovery-test.mjs +31 -0
  8. package/scripts/memory-core-input-test.mjs +10 -0
  9. package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
  10. package/scripts/openai-ws-early-settle-test.mjs +40 -0
  11. package/scripts/parent-abort-link-test.mjs +24 -0
  12. package/scripts/process-lifecycle-test.mjs +80 -22
  13. package/scripts/provider-contract-test.mjs +257 -0
  14. package/scripts/provider-toolcall-test.mjs +172 -10
  15. package/scripts/session-orphan-sweep-test.mjs +27 -1
  16. package/src/lib/keychain-cjs.cjs +36 -23
  17. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
  18. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
  19. package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
  20. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
  22. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
  24. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
  25. package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
  26. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
  27. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
  28. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
  29. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
  30. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
  39. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
  41. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
  42. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
  43. package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
  44. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
  45. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
  46. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
  47. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
  48. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
  49. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
  50. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
  51. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
  52. package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
  53. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
  55. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
  57. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
  58. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
  59. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
  61. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
  62. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
  63. package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
  64. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
  65. package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
  66. package/src/runtime/channels/backends/discord.mjs +6 -6
  67. package/src/runtime/channels/lib/config.mjs +15 -2
  68. package/src/runtime/channels/lib/memory-client.mjs +20 -3
  69. package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
  70. package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
  71. package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
  72. package/src/runtime/channels/lib/worker-main.mjs +16 -5
  73. package/src/runtime/memory/index.mjs +24 -198
  74. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
  75. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
  76. package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
  77. package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
  78. package/src/runtime/memory/lib/query-handlers.mjs +2 -122
  79. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
  80. package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
  81. package/src/runtime/search/lib/http-fetch.mjs +1 -1
  82. package/src/runtime/shared/config.mjs +58 -13
  83. package/src/runtime/shared/llm/cost.mjs +14 -4
  84. package/src/runtime/shared/memory-snapshot.mjs +236 -0
  85. package/src/runtime/shared/process-lifecycle.mjs +92 -19
  86. package/src/runtime/shared/process-shutdown.mjs +6 -0
  87. package/src/runtime/shared/resource-admission.mjs +7 -2
  88. package/src/session-runtime/channel-config-api.mjs +7 -7
  89. package/src/session-runtime/config-lifecycle.mjs +20 -17
  90. package/src/session-runtime/cwd-plugins.mjs +9 -7
  91. package/src/session-runtime/env.mjs +1 -2
  92. package/src/session-runtime/hitch-profile.mjs +45 -0
  93. package/src/session-runtime/lifecycle-api.mjs +36 -9
  94. package/src/session-runtime/mcp-glue.mjs +6 -11
  95. package/src/session-runtime/provider-init-key.mjs +17 -0
  96. package/src/session-runtime/runtime-core.mjs +44 -103
  97. package/src/session-runtime/runtime-paths.mjs +20 -0
  98. package/src/session-runtime/runtime-tool-routing.mjs +55 -0
  99. package/src/session-runtime/tool-catalog-data.mjs +51 -0
  100. package/src/session-runtime/tool-catalog.mjs +15 -89
  101. package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
  102. package/src/standalone/agent-tool/worker-rows.mjs +93 -0
  103. package/src/standalone/agent-tool.mjs +12 -162
  104. package/src/standalone/channel-admin.mjs +29 -0
  105. package/src/tui/App.jsx +11 -5
  106. package/src/tui/dist/index.mjs +202 -65
  107. package/src/tui/engine/session-api-ext.mjs +37 -4
  108. package/src/tui/index.jsx +1 -0
  109. package/src/tui/lib/voice-setup.mjs +5 -5
  110. package/scripts/_devtools-stub.mjs +0 -1
  111. package/src/runtime/lib/keychain-cjs.cjs +0 -288
@@ -0,0 +1,73 @@
1
+ import { isKnownProvider } from '../provider-admin.mjs';
2
+ import {
3
+ DEFAULT_AGENT_PRESETS,
4
+ DEFAULT_PROVIDER,
5
+ } from './tool-def.mjs';
6
+ import {
7
+ agentPresetName,
8
+ clean,
9
+ findPreset,
10
+ normalizeAgentName,
11
+ normalizeAgentRoute,
12
+ synthesizePreset,
13
+ } from './helpers.mjs';
14
+
15
+ export function resolveAgentSpawnPreset(config, args = {}) {
16
+ if (args.provider && args.model) {
17
+ return {
18
+ presetName: args.preset || '__direct__',
19
+ preset: {
20
+ id: '__direct__',
21
+ name: '__DIRECT__',
22
+ type: 'agent',
23
+ provider: clean(args.provider),
24
+ model: clean(args.model),
25
+ effort: clean(args.effort) || undefined,
26
+ fast: args.fast === true,
27
+ tools: 'full',
28
+ },
29
+ };
30
+ }
31
+
32
+ const agentName = normalizeAgentName(args.agent);
33
+ const configuredDefault = clean(config?.defaultProvider);
34
+ const fallbackProvider = configuredDefault && isKnownProvider(configuredDefault)
35
+ ? configuredDefault
36
+ : DEFAULT_PROVIDER;
37
+ const workflowSlot = agentName === 'explore' ? 'explorer'
38
+ : (agentName === 'maintainer' ? 'memory' : '');
39
+ const maintenanceSlot = agentName === 'explore' ? 'explore'
40
+ : (agentName === 'maintainer' ? 'memory' : '');
41
+ const agentRoute = !clean(args.preset)
42
+ ? (normalizeAgentRoute(config?.agents?.[agentName], fallbackProvider)
43
+ || (agentName === 'maintainer' ? normalizeAgentRoute(config?.agents?.maintenance, fallbackProvider) : null)
44
+ || normalizeAgentRoute(config?.workflowRoutes?.[workflowSlot], fallbackProvider)
45
+ || normalizeAgentRoute(config?.maintenance?.[maintenanceSlot], fallbackProvider))
46
+ : null;
47
+ if (agentRoute) {
48
+ return {
49
+ presetName: agentPresetName(agentName),
50
+ preset: {
51
+ id: `agent-${agentName}`,
52
+ name: agentPresetName(agentName),
53
+ type: 'agent',
54
+ provider: agentRoute.provider,
55
+ model: agentRoute.model,
56
+ effort: agentRoute.effort,
57
+ fast: agentRoute.fast === true,
58
+ tools: 'full',
59
+ },
60
+ };
61
+ }
62
+
63
+ const mainPreset = !clean(args.preset) && (agentName === 'explore' || agentName === 'maintainer')
64
+ ? findPreset(config, config?.default)
65
+ : null;
66
+ if (mainPreset) return { presetName: mainPreset.id || mainPreset.name, preset: mainPreset };
67
+
68
+ const presetName = clean(args.preset) || DEFAULT_AGENT_PRESETS[agentName];
69
+ if (!presetName) throw new Error(`agent: agent "${agentName}" has no model assignment`);
70
+ const preset = findPreset(config, presetName) || synthesizePreset(config, presetName);
71
+ if (!preset) throw new Error(`agent: preset "${presetName}" not found`);
72
+ return { presetName, preset };
73
+ }
@@ -0,0 +1,93 @@
1
+ import { clean, positiveInt } from './helpers.mjs';
2
+
3
+ export const TAG_TOMBSTONE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
4
+ const MAX_TAG_TOMBSTONES = 500;
5
+
6
+ export function workerRowKey(row = {}) {
7
+ return clean(row.sessionId) || clean(row.tag);
8
+ }
9
+
10
+ export function workerRowTime(row = {}) {
11
+ return Date.parse(row.updatedAt || row.finishedAt || row.lastUsedAt || row.createdAt || '') || 0;
12
+ }
13
+
14
+ export function isTerminalWorkerStatus(status) {
15
+ return /^(idle|closed|completed|failed|error|cancelled|canceled|killed|timeout)$/i.test(clean(status));
16
+ }
17
+
18
+ export function tagTombstoneKey(row = {}) {
19
+ return `${positiveInt(row.clientHostPid) || 0}\0${clean(row.tag)}`;
20
+ }
21
+
22
+ export function normalizeTagTombstones(value, { cap = true, priorityKeys = null } = {}) {
23
+ const source = Array.isArray(value?.tombstones)
24
+ ? value.tombstones
25
+ : (value?.tombstones && typeof value.tombstones === 'object'
26
+ ? Object.values(value.tombstones)
27
+ : []);
28
+ const now = Date.now();
29
+ const cutoff = now - TAG_TOMBSTONE_TTL_MS;
30
+ const rows = source
31
+ .filter((row) => row && typeof row === 'object')
32
+ .map((row) => {
33
+ const parsedReapedAt = Date.parse(clean(row.reapedAt)) || 0;
34
+ return {
35
+ tag: clean(row.tag),
36
+ agent: clean(row.agent) || null,
37
+ cwd: clean(row.cwd) || null,
38
+ clientHostPid: positiveInt(row.clientHostPid),
39
+ // A future clock must not outrank tombstones created by this process.
40
+ reapedAt: parsedReapedAt ? new Date(Math.min(parsedReapedAt, now)).toISOString() : null,
41
+ };
42
+ })
43
+ .filter((row) => row.tag && row.reapedAt && (Date.parse(row.reapedAt) || 0) >= cutoff)
44
+ .sort((a, b) => {
45
+ const aPriority = priorityKeys?.has(tagTombstoneKey(a)) ? 1 : 0;
46
+ const bPriority = priorityKeys?.has(tagTombstoneKey(b)) ? 1 : 0;
47
+ return bPriority - aPriority
48
+ || (Date.parse(b.reapedAt) || 0) - (Date.parse(a.reapedAt) || 0);
49
+ });
50
+ return cap ? rows.slice(0, MAX_TAG_TOMBSTONES) : rows;
51
+ }
52
+
53
+ export function applyWorkerRowUpsert(byKey, normalized) {
54
+ if (!normalized) return;
55
+ const key = workerRowKey(normalized);
56
+ if (!key) return;
57
+ const prev = byKey.get(key) || {};
58
+ const merged = { ...prev, ...normalized };
59
+ for (const field of ['agent', 'provider', 'model', 'preset', 'effort', 'fast', 'clientHostPid', 'cwd', 'task_id', 'permission', 'toolPermission']) {
60
+ if ((merged[field] === null || merged[field] === '') && prev[field] != null && prev[field] !== '') {
61
+ merged[field] = prev[field];
62
+ }
63
+ }
64
+ byKey.set(key, {
65
+ ...merged,
66
+ createdAt: normalized.createdAt || prev.createdAt || new Date().toISOString(),
67
+ updatedAt: normalized.updatedAt || new Date().toISOString(),
68
+ });
69
+ }
70
+
71
+ export function workerRowToSession(row = {}) {
72
+ return {
73
+ id: row.sessionId,
74
+ agentTag: row.tag,
75
+ agent: row.agent || null,
76
+ provider: row.provider || null,
77
+ model: row.model || null,
78
+ presetName: row.preset || null,
79
+ effort: row.effort || null,
80
+ fast: row.fast === true,
81
+ status: row.status || 'idle',
82
+ stage: row.stage || row.status || 'idle',
83
+ createdAt: row.createdAt || null,
84
+ updatedAt: row.updatedAt || null,
85
+ lastUsedAt: row.lastUsedAt || null,
86
+ clientHostPid: row.clientHostPid || null,
87
+ cwd: row.cwd || null,
88
+ permission: row.permission || null,
89
+ toolPermission: row.toolPermission || null,
90
+ messageCount: Math.max(0, Number(row.messages || 0)),
91
+ toolCount: Math.max(0, Number(row.tools || 0)),
92
+ };
93
+ }
@@ -28,32 +28,25 @@ import {
28
28
  } from '../runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs';
29
29
  import { buildAgentTaskProgressFields } from './agent-task-status.mjs';
30
30
  import { AGENT_OWNER } from '../runtime/agent/orchestrator/agent-owner.mjs';
31
- import { isKnownProvider } from './provider-admin.mjs';
32
31
  import {
33
32
  ACTIVE_STAGES,
34
33
  AGENT_TOOL,
35
- DEFAULT_AGENT_PRESETS,
36
- DEFAULT_PROVIDER,
37
34
  WORKER_INDEX_FILE,
38
35
  } from './agent-tool/tool-def.mjs';
39
36
  import {
40
- agentPresetName,
41
37
  agentScope,
42
38
  agentTagOf,
43
39
  clean,
44
40
  clearAgentStatuslineRoute,
45
41
  envTimeoutMs,
46
- findPreset,
47
42
  nonNegativeInt,
48
43
  normalizeAgentName,
49
- normalizeAgentRoute,
50
44
  positiveInt,
51
45
  presetKey,
52
46
  readAgentFrontmatterPermission,
53
47
  resolvePrompt,
54
48
  rowMatchesContext,
55
49
  sessionMatchesContext,
56
- synthesizePreset,
57
50
  terminalPidForContext,
58
51
  withCwdHeader,
59
52
  writeAgentStatuslineRoute,
@@ -61,10 +54,22 @@ import {
61
54
  import { abnormalEmptyFinishError, renderResult } from './agent-tool/render.mjs';
62
55
  import { createProviderInit } from './agent-tool/provider-init.mjs';
63
56
  import { createNotify } from './agent-tool/notify.mjs';
57
+ import { resolveAgentSpawnPreset } from './agent-tool/spawn-preset.mjs';
58
+ import {
59
+ TAG_TOMBSTONE_TTL_MS,
60
+ applyWorkerRowUpsert,
61
+ isTerminalWorkerStatus,
62
+ normalizeTagTombstones,
63
+ tagTombstoneKey,
64
+ workerRowKey,
65
+ workerRowTime,
66
+ workerRowToSession,
67
+ } from './agent-tool/worker-rows.mjs';
64
68
  import { resolveAgentTerminalReapMs } from '../session-runtime/config-helpers.mjs';
65
69
  // Re-export the static tool descriptor so importers of this facade keep the
66
70
  // identical public surface (`import { AGENT_TOOL } from './agent-tool.mjs'`).
67
71
  export { AGENT_TOOL };
72
+ export { resolveAgentSpawnPreset } from './agent-tool/spawn-preset.mjs';
68
73
 
69
74
  ensureProcessListenerHeadroom(64);
70
75
 
@@ -77,72 +82,6 @@ const STANDALONE_SOURCE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)),
77
82
  // cap and restore strictly-unbounded prep.
78
83
  const DEFAULT_SPAWN_PREP_TIMEOUT_MS = envTimeoutMs('MIXDOG_AGENT_SPAWN_PREP_TIMEOUT_MS', 120_000);
79
84
 
80
- const TAG_TOMBSTONE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
81
- const MAX_TAG_TOMBSTONES = 500;
82
-
83
- /**
84
- * Resolve the route for a public agent spawn. Explore and Maintainer inherit
85
- * Main only after their explicit agent, workflow, and maintenance routes.
86
- */
87
- export function resolveAgentSpawnPreset(config, args = {}) {
88
- if (args.provider && args.model) {
89
- return {
90
- presetName: args.preset || '__direct__',
91
- preset: {
92
- id: '__direct__',
93
- name: '__DIRECT__',
94
- type: 'agent',
95
- provider: clean(args.provider),
96
- model: clean(args.model),
97
- effort: clean(args.effort) || undefined,
98
- fast: args.fast === true,
99
- tools: 'full',
100
- },
101
- };
102
- }
103
-
104
- const agentName = normalizeAgentName(args.agent);
105
- const configuredDefault = clean(config?.defaultProvider);
106
- const fallbackProvider = configuredDefault && isKnownProvider(configuredDefault)
107
- ? configuredDefault
108
- : DEFAULT_PROVIDER;
109
- const workflowSlot = agentName === 'explore' ? 'explorer'
110
- : (agentName === 'maintainer' ? 'memory' : '');
111
- const maintenanceSlot = agentName === 'explore' ? 'explore'
112
- : (agentName === 'maintainer' ? 'memory' : '');
113
- const agentRoute = !clean(args.preset)
114
- ? (normalizeAgentRoute(config?.agents?.[agentName], fallbackProvider)
115
- || (agentName === 'maintainer' ? normalizeAgentRoute(config?.agents?.maintenance, fallbackProvider) : null)
116
- || normalizeAgentRoute(config?.workflowRoutes?.[workflowSlot], fallbackProvider)
117
- || normalizeAgentRoute(config?.maintenance?.[maintenanceSlot], fallbackProvider))
118
- : null;
119
- if (agentRoute) {
120
- return {
121
- presetName: agentPresetName(agentName),
122
- preset: {
123
- id: `agent-${agentName}`,
124
- name: agentPresetName(agentName),
125
- type: 'agent',
126
- provider: agentRoute.provider,
127
- model: agentRoute.model,
128
- effort: agentRoute.effort,
129
- fast: agentRoute.fast === true,
130
- tools: 'full',
131
- },
132
- };
133
- }
134
-
135
- const mainPreset = !clean(args.preset) && (agentName === 'explore' || agentName === 'maintainer')
136
- ? findPreset(config, config?.default)
137
- : null;
138
- if (mainPreset) return { presetName: mainPreset.id || mainPreset.name, preset: mainPreset };
139
-
140
- const presetName = clean(args.preset) || DEFAULT_AGENT_PRESETS[agentName];
141
- if (!presetName) throw new Error(`agent: agent "${agentName}" has no model assignment`);
142
- const preset = findPreset(config, presetName) || synthesizePreset(config, presetName);
143
- if (!preset) throw new Error(`agent: preset "${presetName}" not found`);
144
- return { presetName, preset };
145
- }
146
85
  export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultCwd, onSubagentEvent }) {
147
86
  // Optional bridge to the standard hook bus for SubagentStart / SubagentStop.
148
87
  // Best-effort: a hook error must never affect worker spawn/finish.
@@ -160,18 +99,6 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
160
99
  return dataDir ? resolve(dataDir, WORKER_INDEX_FILE) : null;
161
100
  }
162
101
 
163
- function workerRowKey(row = {}) {
164
- return clean(row.sessionId) || clean(row.tag);
165
- }
166
-
167
- function workerRowTime(row = {}) {
168
- return Date.parse(row.updatedAt || row.finishedAt || row.lastUsedAt || row.createdAt || '') || 0;
169
- }
170
-
171
- function isTerminalWorkerStatus(status) {
172
- return /^(idle|closed|completed|failed|error|cancelled|canceled|killed|timeout)$/i.test(clean(status));
173
- }
174
-
175
102
  function keepWorkerRow(row = {}) {
176
103
  if (!clean(row.tag) || !clean(row.sessionId)) return false;
177
104
  const t = workerRowTime(row);
@@ -216,41 +143,6 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
216
143
  .filter(keepWorkerRow);
217
144
  }
218
145
 
219
- function normalizeTagTombstones(value, { cap = true, priorityKeys = null } = {}) {
220
- const source = Array.isArray(value?.tombstones)
221
- ? value.tombstones
222
- : (value?.tombstones && typeof value.tombstones === 'object'
223
- ? Object.values(value.tombstones)
224
- : []);
225
- const now = Date.now();
226
- const cutoff = now - TAG_TOMBSTONE_TTL_MS;
227
- const rows = source
228
- .filter((row) => row && typeof row === 'object')
229
- .map((row) => {
230
- const parsedReapedAt = Date.parse(clean(row.reapedAt)) || 0;
231
- return {
232
- tag: clean(row.tag),
233
- agent: clean(row.agent) || null,
234
- cwd: clean(row.cwd) || null,
235
- clientHostPid: positiveInt(row.clientHostPid),
236
- // A future clock must not outrank tombstones created by this process.
237
- reapedAt: parsedReapedAt ? new Date(Math.min(parsedReapedAt, now)).toISOString() : null,
238
- };
239
- })
240
- .filter((row) => row.tag && row.reapedAt && (Date.parse(row.reapedAt) || 0) >= cutoff)
241
- .sort((a, b) => {
242
- const aPriority = priorityKeys?.has(tagTombstoneKey(a)) ? 1 : 0;
243
- const bPriority = priorityKeys?.has(tagTombstoneKey(b)) ? 1 : 0;
244
- return bPriority - aPriority
245
- || (Date.parse(b.reapedAt) || 0) - (Date.parse(a.reapedAt) || 0);
246
- });
247
- return cap ? rows.slice(0, MAX_TAG_TOMBSTONES) : rows;
248
- }
249
-
250
- function tagTombstoneKey(row = {}) {
251
- return `${positiveInt(row.clientHostPid) || 0}\0${clean(row.tag)}`;
252
- }
253
-
254
146
  // Mtime-keyed parse cache for the worker index. A single spawn calls
255
147
  // refreshTagsFromSessions()/resolveTag()/nextTag() which each re-read and
256
148
  // re-JSON.parse this file; across a parallel fanout that is O(spawns^2)
@@ -343,24 +235,6 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
343
235
  }
344
236
  }
345
237
 
346
- function applyWorkerRowUpsert(byKey, normalized) {
347
- if (!normalized) return;
348
- const key = workerRowKey(normalized);
349
- if (!key) return;
350
- const prev = byKey.get(key) || {};
351
- const merged = { ...prev, ...normalized };
352
- for (const field of ['agent', 'provider', 'model', 'preset', 'effort', 'fast', 'clientHostPid', 'cwd', 'task_id', 'permission', 'toolPermission']) {
353
- if ((merged[field] === null || merged[field] === '') && prev[field] != null && prev[field] !== '') {
354
- merged[field] = prev[field];
355
- }
356
- }
357
- byKey.set(key, {
358
- ...merged,
359
- createdAt: normalized.createdAt || prev.createdAt || new Date().toISOString(),
360
- updatedAt: normalized.updatedAt || new Date().toISOString(),
361
- });
362
- }
363
-
364
238
  function flushWorkerIndexMutations() {
365
239
  if (workerIndexFlushTimer) {
366
240
  try { clearImmediate(workerIndexFlushTimer); } catch {}
@@ -420,30 +294,6 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
420
294
  };
421
295
  }
422
296
 
423
- function workerRowToSession(row = {}) {
424
- return {
425
- id: row.sessionId,
426
- agentTag: row.tag,
427
- agent: row.agent || null,
428
- provider: row.provider || null,
429
- model: row.model || null,
430
- presetName: row.preset || null,
431
- effort: row.effort || null,
432
- fast: row.fast === true,
433
- status: row.status || 'idle',
434
- stage: row.stage || row.status || 'idle',
435
- createdAt: row.createdAt || null,
436
- updatedAt: row.updatedAt || null,
437
- lastUsedAt: row.lastUsedAt || null,
438
- clientHostPid: row.clientHostPid || null,
439
- cwd: row.cwd || null,
440
- permission: row.permission || null,
441
- toolPermission: row.toolPermission || null,
442
- messageCount: Math.max(0, Number(row.messages || 0)),
443
- toolCount: Math.max(0, Number(row.tools || 0)),
444
- };
445
- }
446
-
447
297
  function upsertWorkerRow(row, { defer = false } = {}) {
448
298
  const normalized = normalizeWorkerRows({ workers: [row] })[0];
449
299
  if (!normalized) return false;
@@ -217,6 +217,22 @@ export function setChannel({ channelId, backend = null } = {}) {
217
217
  });
218
218
  }
219
219
 
220
+ export async function setChannelAsync({ channelId, backend = null } = {}) {
221
+ const id = String(channelId || '').trim();
222
+ if (!id) throw new Error('channelId is required');
223
+ const targetBackend = backend === 'telegram' || backend === 'discord' ? backend : null;
224
+ return updateChannelsSectionAsync((cfg) => {
225
+ const activeBackend = cfg.backend === 'telegram' ? 'telegram' : 'discord';
226
+ const writeBackend = targetBackend || activeBackend;
227
+ const current = seedBackendChannelIds(resolveChannelEntry(cfg), activeBackend);
228
+ const nextEntry = { ...current };
229
+ if (writeBackend === 'telegram') nextEntry.telegramChatId = id;
230
+ else nextEntry.discordChannelId = id;
231
+ nextEntry.channelId = channelIdForBackend(nextEntry, activeBackend);
232
+ return { ...cfg, channel: nextEntry };
233
+ });
234
+ }
235
+
220
236
  export function setWebhookConfig(patch = {}) {
221
237
  return updateChannelsSection((cfg) => ({
222
238
  ...cfg,
@@ -230,6 +246,19 @@ export function setWebhookConfig(patch = {}) {
230
246
  }));
231
247
  }
232
248
 
249
+ export async function setWebhookConfigAsync(patch = {}) {
250
+ return updateChannelsSectionAsync((cfg) => ({
251
+ ...cfg,
252
+ webhook: {
253
+ ...(cfg.webhook || {}),
254
+ ...(Object.prototype.hasOwnProperty.call(patch, 'enabled') ? { enabled: patch.enabled === true } : {}),
255
+ ...(patch.port ? { port: Number(patch.port) || 3333 } : {}),
256
+ ...(patch.domain ? { domain: String(patch.domain).trim() } : {}),
257
+ ...(patch.ngrokDomain ? { ngrokDomain: String(patch.ngrokDomain).trim() } : {}),
258
+ },
259
+ }));
260
+ }
261
+
233
262
  function validateBackend(name) {
234
263
  const value = String(name || '').trim();
235
264
  if (value !== 'discord' && value !== 'telegram') {
package/src/tui/App.jsx CHANGED
@@ -1889,8 +1889,11 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
1889
1889
  }
1890
1890
  if (channelPrompt.kind === 'webhook-domain') {
1891
1891
  if (!commandText) return false;
1892
- store.setWebhookConfig?.({ ngrokDomain: commandText });
1893
- resumeAfterChannelPrompt(channelPrompt);
1892
+ Promise.resolve(store.setWebhookConfig?.({ ngrokDomain: commandText }))
1893
+ .then(() => resumeAfterChannelPrompt(channelPrompt))
1894
+ .catch((e) => {
1895
+ store.pushNotice(`webhook config update failed: ${e?.message || e}`, 'error');
1896
+ });
1894
1897
  return true;
1895
1898
  }
1896
1899
  const parts = commandText.split('|').map((part) => part.trim());
@@ -1900,11 +1903,14 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
1900
1903
  // field) so old muscle memory does not break.
1901
1904
  const isPipe = parts.length > 1;
1902
1905
  const channelId = isPipe ? parts[1] : parts[0];
1903
- store.setChannel({
1906
+ Promise.resolve(store.setChannel({
1904
1907
  channelId,
1905
1908
  backend: channelPrompt.backend,
1906
- });
1907
- resumeAfterChannelPrompt(channelPrompt);
1909
+ }))
1910
+ .then(() => resumeAfterChannelPrompt(channelPrompt))
1911
+ .catch((e) => {
1912
+ store.pushNotice(`channel save failed: ${e?.message || e}`, 'error');
1913
+ });
1908
1914
  return true;
1909
1915
  }
1910
1916
  if (channelPrompt.kind === 'schedule-add') {