mixdog 0.9.67 → 0.9.69

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 (73) hide show
  1. package/package.json +6 -4
  2. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -6
  3. package/src/runtime/agent/orchestrator/context/collect.mjs +42 -27
  4. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +1 -1
  5. package/src/runtime/agent/orchestrator/providers/gemini.mjs +0 -6
  6. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +161 -0
  7. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +8 -204
  8. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +26 -0
  9. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +25 -13
  10. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +3 -0
  11. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -11
  12. package/src/runtime/agent/orchestrator/session/store/listing.mjs +613 -0
  13. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +1 -0
  14. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +10 -2
  15. package/src/runtime/agent/orchestrator/session/store.mjs +9 -575
  16. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +41 -0
  17. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +154 -0
  18. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +78 -18
  19. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +9 -144
  20. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +7 -3
  21. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -3
  22. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +24 -8
  23. package/src/runtime/channels/lib/config.mjs +6 -5
  24. package/src/runtime/channels/lib/owned-runtime.mjs +65 -5
  25. package/src/runtime/channels/lib/scheduler.mjs +7 -15
  26. package/src/runtime/channels/lib/tool-dispatch.mjs +6 -1
  27. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +6 -2
  28. package/src/runtime/channels/lib/webhook.mjs +36 -46
  29. package/src/runtime/channels/lib/worker-main.mjs +45 -92
  30. package/src/runtime/shared/automation-attachments.mjs +72 -0
  31. package/src/runtime/shared/automation-workflow.mjs +41 -0
  32. package/src/runtime/shared/schedule-session-run.mjs +10 -1
  33. package/src/runtime/shared/schedules-db.mjs +18 -3
  34. package/src/runtime/shared/webhook-session-run.mjs +57 -0
  35. package/src/runtime/shared/webhooks-db.mjs +19 -3
  36. package/src/session-runtime/channel-config-api.mjs +8 -1
  37. package/src/session-runtime/lifecycle-api.mjs +7 -0
  38. package/src/session-runtime/memory-daemon-probe.mjs +61 -0
  39. package/src/session-runtime/model-capabilities.mjs +6 -4
  40. package/src/session-runtime/notification-bus.mjs +82 -0
  41. package/src/session-runtime/provider-auth-api.mjs +16 -1
  42. package/src/session-runtime/provider-usage.mjs +3 -0
  43. package/src/session-runtime/remote-control.mjs +166 -0
  44. package/src/session-runtime/remote-transcript.mjs +126 -0
  45. package/src/session-runtime/remote-transition-queue.mjs +14 -0
  46. package/src/session-runtime/runtime-core.mjs +184 -660
  47. package/src/session-runtime/runtime-tunables.mjs +57 -0
  48. package/src/session-runtime/self-update.mjs +129 -0
  49. package/src/session-runtime/skills-api.mjs +77 -0
  50. package/src/session-runtime/tool-surface.mjs +83 -0
  51. package/src/session-runtime/workflow-agents-api.mjs +206 -3
  52. package/src/session-runtime/workflow.mjs +84 -17
  53. package/src/standalone/agent-tool/worker-index.mjs +287 -0
  54. package/src/standalone/agent-tool.mjs +22 -346
  55. package/src/standalone/agent-watchdog-registry.mjs +101 -0
  56. package/src/standalone/channel-admin.mjs +36 -1
  57. package/src/standalone/channel-daemon-client.mjs +5 -1
  58. package/src/standalone/channel-daemon-transport.mjs +112 -20
  59. package/src/standalone/channel-daemon.mjs +6 -4
  60. package/src/standalone/channel-worker.mjs +7 -13
  61. package/src/tui/App.jsx +2 -32
  62. package/src/tui/app/channel-pickers.mjs +2 -143
  63. package/src/tui/app/slash-commands.mjs +1 -3
  64. package/src/tui/app/slash-dispatch.mjs +3 -3
  65. package/src/tui/components/StatusLine.jsx +6 -5
  66. package/src/tui/dist/index.mjs +162 -259
  67. package/src/tui/engine/labels.mjs +0 -8
  68. package/src/tui/engine/session-api-ext.mjs +45 -10
  69. package/src/tui/engine/turn-watchdog.mjs +92 -0
  70. package/src/tui/engine/turn.mjs +18 -70
  71. package/src/tui/engine.mjs +14 -1
  72. package/src/workflows/default/WORKFLOW.md +1 -1
  73. package/src/workflows/solo/WORKFLOW.md +1 -1
@@ -0,0 +1,166 @@
1
+ // Remote (channel relay) lifecycle for a session: claim, release and stop.
2
+ // Remote mode is opt-in per session — only a session that turns it on boots the
3
+ // channel worker and contends for the machine-global bridge seat. Extracted
4
+ // from runtime-core, which injects the mutable session/remote state it owns.
5
+ import { startMemoryDaemonEagerly } from './memory-daemon-probe.mjs';
6
+
7
+ export function createRemoteControl({
8
+ getSession,
9
+ isRemoteEnabled,
10
+ setRemoteEnabled,
11
+ getRemoteSessionId,
12
+ setRemoteSessionId,
13
+ setRemoteClaimPending,
14
+ isRemoteClaimPending,
15
+ isCloseRequested,
16
+ clearChannelStartTimer,
17
+ remoteTransitions,
18
+ channels,
19
+ channelsEnabled,
20
+ hasActiveAutomation,
21
+ flushBackendSave,
22
+ invokeChannelStart,
23
+ createCurrentSession,
24
+ ensureRemoteTranscriptWriter,
25
+ getTranscriptPath,
26
+ emitRemoteStateChange,
27
+ getMemoryModule,
28
+ bootProfile,
29
+ envFlag,
30
+ }) {
31
+ // FORCE-TAKEOVER for an explicit claim: it always (re)claims the seat and
32
+ // rebinds forwarding to this session, even when the worker already runs
33
+ // (`/remote` re-issued after another session took the seat).
34
+ function startRemote(options = {}) {
35
+ const intent = options?.intent === 'auto' ? 'auto' : 'explicit';
36
+ if (intent === 'auto') {
37
+ // Auto-start attempts a claim-if-vacant: remoteEnabled flips only when the
38
+ // worker reports it actually acquired the seat, so a live owner elsewhere
39
+ // leaves this session non-remote.
40
+ setRemoteClaimPending(true);
41
+ } else {
42
+ setRemoteEnabled(true);
43
+ // Explicit toggle: the CURRENT session owns the relay (lazy create adopts
44
+ // inside ensureRemoteTranscriptWriter when no session exists yet).
45
+ setRemoteSessionId(getSession()?.id || null);
46
+ }
47
+ // The worker forwards transcript ingests to the memory service, so its port
48
+ // must be live before the first ingest.
49
+ startMemoryDaemonEagerly({ getMemoryModule, bootProfile });
50
+ // Publish the session record + transcript file BEFORE the worker's
51
+ // activate-time discovery polls, so forwarding binds to THIS terminal
52
+ // instead of a stale neighbour. A no-op in lazy mode; the turn-start rebind
53
+ // covers that case.
54
+ ensureRemoteTranscriptWriter();
55
+ if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
56
+ bootProfile('channels:start-skipped');
57
+ return true;
58
+ }
59
+ if (isCloseRequested()) return true;
60
+ clearChannelStartTimer();
61
+ bootProfile('channels:start-scheduled', { delayMs: 0, immediate: true });
62
+ return remoteTransitions.run(async () => {
63
+ try {
64
+ // The channels-module toggle gates MESSAGING only: automation still
65
+ // boots the worker, which runs headless when messaging is off.
66
+ if (!channelsEnabled() && !(await hasActiveAutomation().catch(() => false))) {
67
+ bootProfile('channels:start-disabled');
68
+ setRemoteClaimPending(false);
69
+ return isRemoteEnabled();
70
+ }
71
+ // A backend switch may still be writing; drain it before the worker
72
+ // reads config rather than racing a sync lock wait.
73
+ try { await flushBackendSave(); } catch { /* best-effort */ }
74
+ // Yield before the create/transcript/fork chain so Ink's queued render
75
+ // and input handling are not starved by this detached chain.
76
+ await new Promise((resolve) => setImmediate(resolve));
77
+ // Immediate-occupancy guarantee: a freshly forked worker runs transcript
78
+ // discovery inside its own start(), so the session must exist first.
79
+ // Idempotent; on failure we still claim (bind resolves on first turn).
80
+ try { await createCurrentSession('remote-start'); } catch (error) {
81
+ bootProfile('channels:remote-session-create-failed', { error: error?.message || String(error) });
82
+ }
83
+ ensureRemoteTranscriptWriter();
84
+ // Re-check after the awaits: stop/supersede or runtime close may have
85
+ // landed mid-chain — never boot for a session that turned remote off.
86
+ if ((!isRemoteEnabled() && !isRemoteClaimPending()) || isCloseRequested()) {
87
+ setRemoteClaimPending(false);
88
+ return false;
89
+ }
90
+ // The fork reads process.env synchronously, so set the inherited intent
91
+ // immediately before it and restore right after: the pollution window is
92
+ // this fork instead of the whole boot chain.
93
+ const previousIntent = process.env.MIXDOG_REMOTE_INTENT;
94
+ try {
95
+ process.env.MIXDOG_REMOTE_INTENT = intent;
96
+ await invokeChannelStart();
97
+ } finally {
98
+ if (previousIntent === undefined) delete process.env.MIXDOG_REMOTE_INTENT;
99
+ else process.env.MIXDOG_REMOTE_INTENT = previousIntent;
100
+ }
101
+ if ((!isRemoteEnabled() && !isRemoteClaimPending()) || isCloseRequested()) {
102
+ setRemoteClaimPending(false);
103
+ return false;
104
+ }
105
+ // Explicit claims are unconditional; auto-start dispatches the same
106
+ // activation as claim-if-vacant so it promotes an automation-only daemon
107
+ // without stealing a live remote owner.
108
+ await channels.execute('activate_channel_bridge', {
109
+ active: true,
110
+ claimIfVacant: intent === 'auto',
111
+ sessionId: getSession()?.id || getRemoteSessionId() || null,
112
+ transcriptPath: getTranscriptPath(),
113
+ });
114
+ // The worker's acquire/supersede notification now owns the transition.
115
+ setRemoteClaimPending(false);
116
+ return isRemoteEnabled();
117
+ } catch (error) {
118
+ setRemoteClaimPending(false);
119
+ if (isRemoteEnabled()) {
120
+ setRemoteEnabled(false);
121
+ setRemoteSessionId(null);
122
+ await channels.stop('remote-claim-failed').catch(() => {});
123
+ emitRemoteStateChange(false, 'claim-failed');
124
+ }
125
+ bootProfile('channels:claim-failed', { error: error?.message || String(error) });
126
+ return false;
127
+ }
128
+ });
129
+ }
130
+
131
+ // Explicit user OFF is a desired-state command, not a toggle retry: deactivate
132
+ // the machine-global bridge before detaching. The promise settles only after
133
+ // detach, keeping the desktop toggle busy until a later ON can reconnect.
134
+ function releaseRemote(reason) {
135
+ const releasingSessionId = getRemoteSessionId() || getSession()?.id || null;
136
+ setRemoteEnabled(false);
137
+ setRemoteSessionId(null);
138
+ setRemoteClaimPending(false);
139
+ clearChannelStartTimer();
140
+ return remoteTransitions.run(async () => {
141
+ try {
142
+ await channels.execute('activate_channel_bridge', { active: false, sessionId: releasingSessionId });
143
+ } catch (error) {
144
+ bootProfile('channels:release-failed', { error: error?.message || String(error) });
145
+ emitRemoteStateChange(false, 'release-failed');
146
+ } finally {
147
+ await channels.stop(reason || 'remote-disabled').catch(() => {});
148
+ }
149
+ return false;
150
+ });
151
+ }
152
+
153
+ // /remote-off and supersede: the runtime keeps running so this never blocks,
154
+ // but the waiting stop path runs the full SIGTERM -> taskkill escalation so the
155
+ // worker cannot linger as a zombie holding the seat.
156
+ function stopRemote(reason) {
157
+ setRemoteEnabled(false);
158
+ setRemoteSessionId(null);
159
+ setRemoteClaimPending(false);
160
+ clearChannelStartTimer();
161
+ channels.stop(reason || 'remote-disabled').catch(() => {});
162
+ return true;
163
+ }
164
+
165
+ return { startRemote, releaseRemote, stopRemote };
166
+ }
@@ -0,0 +1,126 @@
1
+ // Remote transcript binding: the channel worker forwards output by watching a
2
+ // per-session transcript file, so the runtime must (re)create the writer for
3
+ // the CURRENT session+cwd and push the path to the worker whenever the binding
4
+ // can go stale (relay acquire, new session, resume, clear). Extracted from
5
+ // runtime-core; every mutable local it needs is injected as an accessor.
6
+ import { createTranscriptWriter } from '../runtime/shared/transcript-writer.mjs';
7
+ import { mixdogHome } from '../runtime/shared/plugin-paths.mjs';
8
+
9
+ const REBIND_MAX_ATTEMPTS = 3;
10
+
11
+ export function createRemoteTranscript({
12
+ getSession,
13
+ getCwd,
14
+ isRemoteEnabled,
15
+ getRemoteSessionId,
16
+ setRemoteSessionId,
17
+ isCloseRequested,
18
+ channelsEnabled,
19
+ channels,
20
+ mgr,
21
+ bootProfile,
22
+ }) {
23
+ let writer = null;
24
+ let writerKey = '';
25
+ let pendingRebind = false;
26
+
27
+ return {
28
+ get transcriptWriter() { return writer; },
29
+ // Session+cwd identity of the current binding: a change means forwarding
30
+ // must be re-activated for the new transcript.
31
+ get transcriptKey() { return writerKey; },
32
+ get pendingRebind() { return pendingRebind; },
33
+ resetPendingRebind() { pendingRebind = false; },
34
+ ensureRemoteTranscriptWriter,
35
+ pushTranscriptRebind,
36
+ flushPendingTranscriptRebind,
37
+ };
38
+
39
+ // Bind (or refresh) the writer for the CURRENT session+cwd. The owner gate
40
+ // covers lazy create: an unset owner adopts this session, a LIVE owner blocks
41
+ // every other session, and a dead owner hands the relay over.
42
+ function ensureRemoteTranscriptWriter() {
43
+ const session = getSession();
44
+ if (!isRemoteEnabled() || !session?.id) return false;
45
+ const owner = getRemoteSessionId();
46
+ if (!owner) {
47
+ setRemoteSessionId(session.id);
48
+ } else if (session.id !== owner) {
49
+ const ownerSession = mgr.getSession(owner);
50
+ if (ownerSession && ownerSession.closed !== true && ownerSession.status !== 'closed') return false;
51
+ setRemoteSessionId(session.id);
52
+ }
53
+ const cwd = getCwd();
54
+ const key = `${session.id}\u0000${cwd}`;
55
+ if (writerKey !== key) {
56
+ try {
57
+ writer = createTranscriptWriter({
58
+ mixdogHome: mixdogHome(),
59
+ sessionId: session.id,
60
+ cwd,
61
+ pid: process.pid,
62
+ });
63
+ writer.writeSessionRecord();
64
+ writerKey = key;
65
+ } catch (error) {
66
+ process.stderr.write(`mixdog: transcript-writer: init failed: ${error?.message || error}\n`);
67
+ writer = null;
68
+ writerKey = '';
69
+ return false;
70
+ }
71
+ } else {
72
+ // Same binding — refresh updatedAt so worker-side discovery keeps ranking
73
+ // this session as the live parent-chain candidate.
74
+ try { writer?.writeSessionRecord(); } catch { /* discovery hint only */ }
75
+ }
76
+ try { writer?.ensureTranscriptFile(); } catch (error) {
77
+ process.stderr.write(`mixdog: transcript-writer: ensureTranscriptFile failed: ${error?.message || error}\n`);
78
+ }
79
+ return writer != null;
80
+ }
81
+
82
+ // Repoint outbound forwarding at the current transcript. Best-effort: a
83
+ // writer that is not bindable yet (relay acquired before the session exists)
84
+ // defers, and flushPendingTranscriptRebind() re-fires it exactly once.
85
+ function pushTranscriptRebind() {
86
+ if (!isRemoteEnabled()) return;
87
+ if (!ensureRemoteTranscriptWriter()) { pendingRebind = true; return; }
88
+ const transcriptPath = writer?.transcriptPath;
89
+ if (!transcriptPath || !channelsEnabled()) { pendingRebind = true; return; }
90
+ pendingRebind = false;
91
+ executeTranscriptRebind(transcriptPath, 1);
92
+ }
93
+
94
+ // Bounded retry on the idempotent worker op; the final failure surfaces one
95
+ // stderr line so a lost rebind is diagnosable without the env-gated profile.
96
+ function executeTranscriptRebind(transcriptPath, attempt) {
97
+ const onError = (error) => {
98
+ const detail = error?.message || String(error);
99
+ bootProfile('channels:rebind-push-failed', { attempt, error: detail });
100
+ if (attempt < REBIND_MAX_ATTEMPTS && isRemoteEnabled() && !isCloseRequested()) {
101
+ const timer = setTimeout(() => {
102
+ // Drop the chain if remote went away or the writer moved on: re-firing
103
+ // the captured path would rebind forwarding to a stale transcript.
104
+ if (!isRemoteEnabled() || !channelsEnabled()) return;
105
+ if (writer?.transcriptPath !== transcriptPath) return;
106
+ executeTranscriptRebind(transcriptPath, attempt + 1);
107
+ }, 150 * attempt);
108
+ timer.unref?.();
109
+ } else {
110
+ process.stderr.write(`mixdog: channels: rebind_current_transcript failed after ${attempt} attempt(s): ${detail}\n`);
111
+ }
112
+ };
113
+ try {
114
+ void channels.execute('rebind_current_transcript', { transcriptPath }).catch(onError);
115
+ } catch (error) {
116
+ onError(error);
117
+ }
118
+ }
119
+
120
+ // Re-fire a deferred rebind once the writer becomes available; a no-op for
121
+ // already-bound sessions so no unconditional rebind fires per turn.
122
+ function flushPendingTranscriptRebind() {
123
+ if (!pendingRebind || !isRemoteEnabled()) return;
124
+ pushTranscriptRebind();
125
+ }
126
+ }
@@ -0,0 +1,14 @@
1
+ export function createRemoteTransitionQueue() {
2
+ let tail = Promise.resolve();
3
+
4
+ return {
5
+ run(task) {
6
+ const result = tail.then(() => task());
7
+ tail = result.then(() => undefined, () => undefined);
8
+ return result;
9
+ },
10
+ settled() {
11
+ return tail;
12
+ },
13
+ };
14
+ }