@shumkov/orchestra 0.2.0

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.
@@ -0,0 +1,216 @@
1
+ // provenance: polygram@0.17.11 lib/process/process.js (git 746bca6) — verbatim: env prefix WATER_, bridge name water-bridge, vendor path (SHARED-LIB.md).
2
+ /**
3
+ * Abstract Process — one running Claude session, regardless of backend.
4
+ *
5
+ * Subclasses ship per backend:
6
+ * - SdkProcess (lib/process/sdk-process.js) — long-lived
7
+ * @anthropic-ai/claude-agent-sdk Query
8
+ * - TmuxProcess (lib/process/tmux-process.js) — claude TUI hosted
9
+ * inside a tmux session (Phase 2)
10
+ *
11
+ * State machine: spawned → ready → (turn-in-flight | idle) ↔ closed.
12
+ *
13
+ * Public surface mirrors what polygram's handleMessage, slash-commands,
14
+ * autosteer, edit-correction etc. already call on the current SDK pm.
15
+ * Callers don't branch on subclass.
16
+ *
17
+ * Optional methods come in two flavors per the v3 audit:
18
+ * - Async ones MAY throw UnsupportedOperationError. Callers `await` +
19
+ * try/catch around them.
20
+ * - Sync HOT-PATH ones (drainQueue, injectUserMessage) return a
21
+ * sentinel value, NEVER throw. Per R1-F1: autosteer's call site
22
+ * has no try/catch — a throw would crash the message handler.
23
+ *
24
+ * Weighted LRU: each Process advertises its `cost`. The pm evicts
25
+ * to keep Σ cost ≤ budget rather than count ≤ cap. SDK Process cost=1,
26
+ * TmuxProcess cost=3 (per Phase 0 F-spike-2: tmux ~545MB vs SDK ~50MB
27
+ * per session).
28
+ *
29
+ * Phase 0 spike findings — `docs/0.10.0-phase0-spike-findings.md`.
30
+ * Spec — `docs/0.10.0-process-manager-abstraction-plan.md`.
31
+ */
32
+
33
+ 'use strict';
34
+
35
+ const EventEmitter = require('events');
36
+
37
+ class UnsupportedOperationError extends Error {
38
+ constructor(method, backend) {
39
+ super(`Operation ${method} not supported by ${backend} backend`);
40
+ this.name = 'UnsupportedOperationError';
41
+ this.code = 'UNSUPPORTED_OPERATION';
42
+ this.method = method;
43
+ this.backend = backend;
44
+ }
45
+ }
46
+
47
+ class Process extends EventEmitter {
48
+ /**
49
+ * @param {object} opts
50
+ * @param {string} opts.sessionKey polygram session key
51
+ * @param {string|null} opts.chatId
52
+ * @param {string|null} opts.threadId
53
+ * @param {string} opts.label human-readable for logs
54
+ */
55
+ constructor({ sessionKey, chatId, threadId, label } = {}) {
56
+ super();
57
+ if (typeof sessionKey !== 'string' || sessionKey.length === 0) {
58
+ throw new TypeError('Process: sessionKey (string) required');
59
+ }
60
+ // Identity — immutable after construction
61
+ this.sessionKey = sessionKey;
62
+ this.chatId = chatId == null ? null : String(chatId);
63
+ this.threadId = threadId == null ? null : String(threadId);
64
+ this.label = label || `${this.chatId || ''}${this.threadId ? '/' + this.threadId : ''}` || sessionKey;
65
+ // backend identifier — subclass overrides
66
+ this.backend = 'abstract';
67
+
68
+ // Mutable state
69
+ this.closed = false;
70
+ this.inFlight = false;
71
+ this.pendingQueue = [];
72
+ this.claudeSessionId = null;
73
+ }
74
+
75
+ /**
76
+ * Cost weight for LRU eviction (per Phase 0 F-spike-2).
77
+ * Subclass overrides. Defaults to 1 (SDK-equivalent).
78
+ */
79
+ get cost() {
80
+ return 1;
81
+ }
82
+
83
+ // ─── REQUIRED methods — subclass MUST override ─────────────────────
84
+
85
+ /**
86
+ * Cold-spawn this process. Wire up internals; mark ready when
87
+ * the underlying claude session is responsive.
88
+ *
89
+ * @param {object} opts — backend-specific. Typically includes:
90
+ * existingSessionId — for --resume continuity
91
+ * model, effort, cwd, chatConfig, botName — spawn params
92
+ */
93
+ async start(_opts) {
94
+ throw new Error(`${this.constructor.name}.start() must be overridden`);
95
+ }
96
+
97
+ /**
98
+ * Send a user turn. Resolves with a PmSendResult on completion.
99
+ *
100
+ * @param {string} prompt
101
+ * @param {object} [opts]
102
+ * @returns {Promise<PmSendResult>}
103
+ */
104
+ async send(_prompt, _opts) {
105
+ throw new Error(`${this.constructor.name}.send() must be overridden`);
106
+ }
107
+
108
+ /**
109
+ * Close cleanly. Returns when fully torn down.
110
+ * Idempotent.
111
+ *
112
+ * @param {string} [reason]
113
+ */
114
+ async kill(_reason) {
115
+ throw new Error(`${this.constructor.name}.kill() must be overridden`);
116
+ }
117
+
118
+ // ─── OPTIONAL async methods — caller awaits + try/catch ────────────
119
+
120
+ async interrupt() {
121
+ throw new UnsupportedOperationError('interrupt', this.backend);
122
+ }
123
+ async setModel(_model) {
124
+ throw new UnsupportedOperationError('setModel', this.backend);
125
+ }
126
+ async applyFlagSettings(_settings) {
127
+ throw new UnsupportedOperationError('applyFlagSettings', this.backend);
128
+ }
129
+ async setPermissionMode(_mode) {
130
+ throw new UnsupportedOperationError('setPermissionMode', this.backend);
131
+ }
132
+ async resetSession(_opts) {
133
+ throw new UnsupportedOperationError('resetSession', this.backend);
134
+ }
135
+ async getContextUsage() {
136
+ throw new UnsupportedOperationError('getContextUsage', this.backend);
137
+ }
138
+
139
+ // ─── OPTIONAL sync HOT-PATH methods — never throw (R1-F1) ──────────
140
+
141
+ /**
142
+ * Reject all pending turns with the supplied error code.
143
+ * Used by /stop, daemon shutdown, /new.
144
+ *
145
+ * @param {string} [_code='INTERRUPTED']
146
+ * @returns {number} count of pendings drained
147
+ */
148
+ drainQueue(_code = 'INTERRUPTED') {
149
+ return 0;
150
+ }
151
+
152
+ /**
153
+ * Inject a user message into the in-flight turn (autosteer +
154
+ * edit-correction). Returns false if the backend can't inject
155
+ * right now (e.g. no live turn) — caller falls through to normal
156
+ * pm.send queue path.
157
+ *
158
+ * @returns {boolean}
159
+ */
160
+ injectUserMessage(_opts) {
161
+ return false;
162
+ }
163
+
164
+ /**
165
+ * Does this session have a DETACHED background job running (a `run_in_background`
166
+ * shell that outlives the dispatch turn)? Used by ProcessManager._evictLRU to PIN
167
+ * the session — skip it for eviction the same way `inFlight` is skipped — so a live
168
+ * job isn't silently killed under budget pressure. Default: no signal → false.
169
+ * Backends that can detect detached shells (cli) override this. Must be cheap + sync.
170
+ *
171
+ * @returns {boolean}
172
+ */
173
+ hasActiveBackgroundWork() {
174
+ return false;
175
+ }
176
+
177
+ /**
178
+ * 0.13 D1 (S9): does this process have an open interactive question?
179
+ * Backends without the ask feature return false; CliProcess overrides.
180
+ * ProcessManager._evictLRU treats true like inFlight (eviction pin).
181
+ */
182
+ hasOpenQuestions() {
183
+ return false;
184
+ }
185
+
186
+ /**
187
+ * Push priority='now' style steer (rare; legacy of OpenClaw shape).
188
+ * Hot-path-safe.
189
+ *
190
+ * @returns {boolean}
191
+ */
192
+ steer(_text, _opts) {
193
+ return false;
194
+ }
195
+
196
+ /**
197
+ * Fire-and-forget user-message injection regardless of inFlight
198
+ * state. Used by polygram's slash-command paths (/compact, /reload
199
+ * etc) where we want to send a user-shaped message into the
200
+ * underlying claude session BUT NOT wait for the turn to complete.
201
+ *
202
+ * Differs from `injectUserMessage` (which is for mid-stream fold and
203
+ * requires inFlight on tmux) and `send` (which blocks until turn
204
+ * completion). Default returns false; subclasses override.
205
+ *
206
+ * @returns {boolean} true if message was queued/pasted
207
+ */
208
+ fireUserMessage(_text) {
209
+ return false;
210
+ }
211
+ }
212
+
213
+ module.exports = {
214
+ Process,
215
+ UnsupportedOperationError,
216
+ };