happy-imou-cloud 1.1.7 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{setupOfflineReconnection-obypStdD.cjs → BaseReasoningProcessor-01KqA3Kz.cjs} +221 -90
- package/dist/{setupOfflineReconnection-ndObLZk0.mjs → BaseReasoningProcessor-DQE2l7Xu.mjs} +218 -90
- package/dist/{types-BXyraW9R.mjs → api-B5Ui8Fw0.mjs} +263 -173
- package/dist/{types-BSTmyv9d.cjs → api-B8v4tczT.cjs} +287 -195
- package/dist/command-BfIuJmeo.mjs +51 -0
- package/dist/command-D8yNlaDo.cjs +54 -0
- package/dist/future-Dq4Ha1Dn.cjs +24 -0
- package/dist/future-xRdLl3vf.mjs +22 -0
- package/dist/{index-CUmYqKWt.mjs → index-BByhFIIq.mjs} +5981 -7180
- package/dist/{index-DVI4b0mv.cjs → index-BOqJ9hwi.cjs} +6004 -7189
- package/dist/index.cjs +19 -21
- package/dist/index.mjs +19 -21
- package/dist/lib.cjs +3 -2
- package/dist/lib.d.cts +18 -0
- package/dist/lib.d.mts +18 -0
- package/dist/lib.mjs +2 -1
- package/dist/{persistence-BRH9F6RS.cjs → persistence-C33AMdtv.cjs} +10 -6
- package/dist/{persistence-BGsuPqaO.mjs → persistence-CzpZpiL3.mjs} +8 -4
- package/dist/registerKillSessionHandler-BkzQulD9.cjs +435 -0
- package/dist/registerKillSessionHandler-BtSK7IOa.mjs +430 -0
- package/dist/runClaude-CNVufgZb.cjs +3282 -0
- package/dist/runClaude-C_WLfM6c.mjs +3279 -0
- package/dist/runCodex-8eWjTPJr.mjs +1663 -0
- package/dist/runCodex-Dzy8anlX.cjs +1668 -0
- package/dist/{runGemini-C3dDtGOV.cjs → runGemini-CgsVKP7m.cjs} +40 -1377
- package/dist/{runGemini-B-EK_BJQ.mjs → runGemini-nbr0mm-S.mjs} +27 -1364
- package/dist/types-CiliQpqS.mjs +52 -0
- package/dist/types-DVk3crez.cjs +54 -0
- package/package.json +12 -11
- package/scripts/devtools/README.md +9 -0
- package/scripts/devtools/generate-mock-credentials.ts +94 -0
- package/scripts/env-wrapper.cjs +11 -11
- package/scripts/release-smoke.mjs +62 -0
- package/scripts/setup-dev.cjs +4 -4
- package/dist/config-BQNrtwRY.cjs +0 -183
- package/dist/config-Dn99YH37.mjs +0 -173
- package/dist/runCodex-Cez8cuIh.cjs +0 -1143
- package/dist/runCodex-X0BfjcZH.mjs +0 -1140
|
@@ -1,11 +1,123 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var api = require('./types-BSTmyv9d.cjs');
|
|
4
|
-
var node_crypto = require('node:crypto');
|
|
5
3
|
var os = require('node:os');
|
|
6
|
-
var
|
|
7
|
-
var
|
|
4
|
+
var path = require('node:path');
|
|
5
|
+
var api = require('./api-B8v4tczT.cjs');
|
|
6
|
+
var index = require('./index-BOqJ9hwi.cjs');
|
|
7
|
+
var node_events = require('node:events');
|
|
8
|
+
var node_crypto = require('node:crypto');
|
|
9
|
+
|
|
10
|
+
function createSessionMetadata(opts) {
|
|
11
|
+
const state = {
|
|
12
|
+
controlledByUser: false
|
|
13
|
+
};
|
|
14
|
+
const metadata = {
|
|
15
|
+
path: process.cwd(),
|
|
16
|
+
host: os.hostname(),
|
|
17
|
+
version: api.packageJson.version,
|
|
18
|
+
os: os.platform(),
|
|
19
|
+
machineId: opts.machineId,
|
|
20
|
+
homeDir: os.homedir(),
|
|
21
|
+
happyHomeDir: api.configuration.happyCloudHomeDir,
|
|
22
|
+
happyLibDir: index.projectPath(),
|
|
23
|
+
happyToolsDir: path.resolve(index.projectPath(), "tools", "unpacked"),
|
|
24
|
+
startedFromDaemon: opts.startedBy === "daemon",
|
|
25
|
+
hostPid: process.pid,
|
|
26
|
+
startedBy: opts.startedBy || "terminal",
|
|
27
|
+
lifecycleState: "running",
|
|
28
|
+
lifecycleStateSince: Date.now(),
|
|
29
|
+
flavor: opts.flavor
|
|
30
|
+
};
|
|
31
|
+
return { state, metadata };
|
|
32
|
+
}
|
|
8
33
|
|
|
34
|
+
function createOfflineSessionStub(sessionTag) {
|
|
35
|
+
const emitter = new node_events.EventEmitter();
|
|
36
|
+
let metadata = null;
|
|
37
|
+
let agentState = null;
|
|
38
|
+
const stub = {
|
|
39
|
+
sessionId: `offline-${sessionTag}`,
|
|
40
|
+
sendCodexMessage: () => {
|
|
41
|
+
},
|
|
42
|
+
sendAgentMessage: () => {
|
|
43
|
+
},
|
|
44
|
+
sendClaudeSessionMessage: () => {
|
|
45
|
+
},
|
|
46
|
+
keepAlive: () => {
|
|
47
|
+
},
|
|
48
|
+
sendSessionEvent: () => {
|
|
49
|
+
},
|
|
50
|
+
sendSessionDeath: () => {
|
|
51
|
+
},
|
|
52
|
+
updateLifecycleState: () => {
|
|
53
|
+
},
|
|
54
|
+
requestControlTransfer: async () => {
|
|
55
|
+
},
|
|
56
|
+
flush: async () => {
|
|
57
|
+
},
|
|
58
|
+
close: async () => {
|
|
59
|
+
},
|
|
60
|
+
updateMetadata: (handler) => {
|
|
61
|
+
metadata = handler(metadata || {});
|
|
62
|
+
emitter.emit("metadata-updated", metadata);
|
|
63
|
+
},
|
|
64
|
+
updateAgentState: (handler) => {
|
|
65
|
+
agentState = handler(agentState || {});
|
|
66
|
+
emitter.emit("agent-state-updated", agentState);
|
|
67
|
+
},
|
|
68
|
+
getMetadataSnapshot: () => metadata,
|
|
69
|
+
getAgentStateSnapshot: () => agentState,
|
|
70
|
+
waitForMetadataUpdate: async () => metadata,
|
|
71
|
+
onUserMessage: () => {
|
|
72
|
+
},
|
|
73
|
+
rpcHandlerManager: {
|
|
74
|
+
registerHandler: () => {
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
on: emitter.on.bind(emitter),
|
|
78
|
+
once: emitter.once.bind(emitter),
|
|
79
|
+
off: emitter.off.bind(emitter),
|
|
80
|
+
emit: emitter.emit.bind(emitter)
|
|
81
|
+
};
|
|
82
|
+
return stub;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setupOfflineReconnection(opts) {
|
|
86
|
+
const { api: api$1, sessionTag, metadata, state, response, onSessionSwap } = opts;
|
|
87
|
+
let session;
|
|
88
|
+
let reconnectionHandle = null;
|
|
89
|
+
if (!response) {
|
|
90
|
+
session = createOfflineSessionStub(sessionTag);
|
|
91
|
+
reconnectionHandle = api.startOfflineReconnection({
|
|
92
|
+
serverUrl: api.configuration.serverUrl,
|
|
93
|
+
onReconnected: async () => {
|
|
94
|
+
const resp = await api$1.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
95
|
+
if (!resp) throw new Error("Server unavailable");
|
|
96
|
+
const realSession = api$1.sessionSyncClient(resp);
|
|
97
|
+
onSessionSwap(realSession);
|
|
98
|
+
return realSession;
|
|
99
|
+
},
|
|
100
|
+
onNotify: (msg) => {
|
|
101
|
+
console.log(msg);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return { session, reconnectionHandle, isOffline: true };
|
|
105
|
+
} else {
|
|
106
|
+
session = api$1.sessionSyncClient(response);
|
|
107
|
+
return { session, reconnectionHandle: null, isOffline: false };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const INTERACTION_SUPERSEDED_ERROR = "Interaction superseded by new user message";
|
|
112
|
+
const INTERACTION_TIMED_OUT_ERROR = "Interaction timed out waiting for user response";
|
|
113
|
+
const DEFAULT_INTERACTION_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
114
|
+
function getPendingInteractionTimeoutMs() {
|
|
115
|
+
const raw = Number(process.env.HAPPY_INTERACTION_TIMEOUT_MS);
|
|
116
|
+
if (Number.isFinite(raw) && raw > 0) {
|
|
117
|
+
return raw;
|
|
118
|
+
}
|
|
119
|
+
return DEFAULT_INTERACTION_TIMEOUT_MS;
|
|
120
|
+
}
|
|
9
121
|
class BasePermissionHandler {
|
|
10
122
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
11
123
|
session;
|
|
@@ -36,6 +148,7 @@ class BasePermissionHandler {
|
|
|
36
148
|
return;
|
|
37
149
|
}
|
|
38
150
|
this.pendingRequests.delete(response.id);
|
|
151
|
+
this.clearPendingRequestTimeout(pending);
|
|
39
152
|
const result = response.approved ? { decision: response.decision === "approved_for_session" ? "approved_for_session" : "approved" } : { decision: response.decision === "denied" ? "denied" : "abort" };
|
|
40
153
|
pending.resolve(result);
|
|
41
154
|
this.session.updateAgentState((currentState) => {
|
|
@@ -77,6 +190,62 @@ class BasePermissionHandler {
|
|
|
77
190
|
}
|
|
78
191
|
}));
|
|
79
192
|
}
|
|
193
|
+
registerPendingRequest(toolCallId, toolName, input, logSuffix = "") {
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
const pending = {
|
|
196
|
+
resolve,
|
|
197
|
+
reject,
|
|
198
|
+
toolName,
|
|
199
|
+
input
|
|
200
|
+
};
|
|
201
|
+
pending.timeoutHandle = setTimeout(() => {
|
|
202
|
+
this.handlePendingRequestTimeout(toolCallId, pending);
|
|
203
|
+
}, getPendingInteractionTimeoutMs());
|
|
204
|
+
this.pendingRequests.set(toolCallId, pending);
|
|
205
|
+
this.addPendingRequestToState(toolCallId, toolName, input);
|
|
206
|
+
api.logger.debug(`${this.getLogPrefix()} Permission request sent for tool: ${toolName} (${toolCallId})${logSuffix}`);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
hasPendingRequests() {
|
|
210
|
+
return this.pendingRequests.size > 0;
|
|
211
|
+
}
|
|
212
|
+
supersedePendingRequests(reason = INTERACTION_SUPERSEDED_ERROR) {
|
|
213
|
+
const pendingSnapshot = Array.from(this.pendingRequests.entries());
|
|
214
|
+
if (pendingSnapshot.length === 0) {
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
this.pendingRequests.clear();
|
|
218
|
+
const completedAt = Date.now();
|
|
219
|
+
for (const [, pending] of pendingSnapshot) {
|
|
220
|
+
this.clearPendingRequestTimeout(pending);
|
|
221
|
+
pending.resolve({ decision: "abort" });
|
|
222
|
+
}
|
|
223
|
+
this.session.updateAgentState((currentState) => {
|
|
224
|
+
const requests = { ...currentState.requests || {} };
|
|
225
|
+
const completedRequests = { ...currentState.completedRequests || {} };
|
|
226
|
+
for (const [id, request] of Object.entries(requests)) {
|
|
227
|
+
if (request.requestKind === "selection") {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
completedRequests[id] = {
|
|
231
|
+
...request,
|
|
232
|
+
completedAt,
|
|
233
|
+
status: "denied",
|
|
234
|
+
reason,
|
|
235
|
+
decision: "abort",
|
|
236
|
+
requestKind: request.requestKind || "permission"
|
|
237
|
+
};
|
|
238
|
+
delete requests[id];
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
...currentState,
|
|
242
|
+
requests,
|
|
243
|
+
completedRequests
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
api.logger.debug(`${this.getLogPrefix()} Superseded ${pendingSnapshot.length} pending permission request(s)`);
|
|
247
|
+
return pendingSnapshot.length;
|
|
248
|
+
}
|
|
80
249
|
/**
|
|
81
250
|
* Reset state for new sessions.
|
|
82
251
|
* This method is idempotent - safe to call multiple times.
|
|
@@ -92,6 +261,7 @@ class BasePermissionHandler {
|
|
|
92
261
|
this.pendingRequests.clear();
|
|
93
262
|
for (const [id, pending] of pendingSnapshot) {
|
|
94
263
|
try {
|
|
264
|
+
this.clearPendingRequestTimeout(pending);
|
|
95
265
|
pending.reject(new Error("Session reset"));
|
|
96
266
|
} catch (err) {
|
|
97
267
|
api.logger.debug(`${this.getLogPrefix()} Error rejecting pending request ${id}:`, err);
|
|
@@ -119,6 +289,50 @@ class BasePermissionHandler {
|
|
|
119
289
|
this.isResetting = false;
|
|
120
290
|
}
|
|
121
291
|
}
|
|
292
|
+
clearPendingRequestTimeout(pending) {
|
|
293
|
+
if (pending?.timeoutHandle) {
|
|
294
|
+
clearTimeout(pending.timeoutHandle);
|
|
295
|
+
pending.timeoutHandle = void 0;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
handlePendingRequestTimeout(toolCallId, pending) {
|
|
299
|
+
const active = this.pendingRequests.get(toolCallId);
|
|
300
|
+
if (!active || active !== pending) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
this.pendingRequests.delete(toolCallId);
|
|
304
|
+
this.clearPendingRequestTimeout(active);
|
|
305
|
+
active.resolve({ decision: "abort" });
|
|
306
|
+
this.session.updateAgentState((currentState) => {
|
|
307
|
+
const request = currentState.requests?.[toolCallId] || {
|
|
308
|
+
tool: active.toolName,
|
|
309
|
+
arguments: active.input,
|
|
310
|
+
createdAt: Date.now(),
|
|
311
|
+
requestKind: "permission"
|
|
312
|
+
};
|
|
313
|
+
const { [toolCallId]: _, ...remainingRequests } = currentState.requests || {};
|
|
314
|
+
return {
|
|
315
|
+
...currentState,
|
|
316
|
+
requests: remainingRequests,
|
|
317
|
+
completedRequests: {
|
|
318
|
+
...currentState.completedRequests,
|
|
319
|
+
[toolCallId]: {
|
|
320
|
+
...request,
|
|
321
|
+
completedAt: Date.now(),
|
|
322
|
+
status: "canceled",
|
|
323
|
+
reason: INTERACTION_TIMED_OUT_ERROR,
|
|
324
|
+
decision: "abort",
|
|
325
|
+
requestKind: request.requestKind || "permission"
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
this.session.sendSessionEvent({
|
|
331
|
+
type: "message",
|
|
332
|
+
message: "Pending interaction timed out waiting for a response. Send a new message to continue."
|
|
333
|
+
});
|
|
334
|
+
api.logger.debug(`${this.getLogPrefix()} Permission request timed out for ${active.toolName} (${toolCallId})`);
|
|
335
|
+
}
|
|
122
336
|
}
|
|
123
337
|
|
|
124
338
|
class BaseReasoningProcessor {
|
|
@@ -315,93 +529,10 @@ class BaseReasoningProcessor {
|
|
|
315
529
|
}
|
|
316
530
|
}
|
|
317
531
|
|
|
318
|
-
function createSessionMetadata(opts) {
|
|
319
|
-
const state = {
|
|
320
|
-
controlledByUser: false
|
|
321
|
-
};
|
|
322
|
-
const metadata = {
|
|
323
|
-
path: process.cwd(),
|
|
324
|
-
host: os.hostname(),
|
|
325
|
-
version: api.packageJson.version,
|
|
326
|
-
os: os.platform(),
|
|
327
|
-
machineId: opts.machineId,
|
|
328
|
-
homeDir: os.homedir(),
|
|
329
|
-
happyHomeDir: api.configuration.happyCloudHomeDir,
|
|
330
|
-
happyLibDir: index.projectPath(),
|
|
331
|
-
happyToolsDir: node_path.resolve(index.projectPath(), "tools", "unpacked"),
|
|
332
|
-
startedFromDaemon: opts.startedBy === "daemon",
|
|
333
|
-
hostPid: process.pid,
|
|
334
|
-
startedBy: opts.startedBy || "terminal",
|
|
335
|
-
lifecycleState: "running",
|
|
336
|
-
lifecycleStateSince: Date.now(),
|
|
337
|
-
flavor: opts.flavor
|
|
338
|
-
};
|
|
339
|
-
return { state, metadata };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function createOfflineSessionStub(sessionTag) {
|
|
343
|
-
return {
|
|
344
|
-
sessionId: `offline-${sessionTag}`,
|
|
345
|
-
sendCodexMessage: () => {
|
|
346
|
-
},
|
|
347
|
-
sendAgentMessage: () => {
|
|
348
|
-
},
|
|
349
|
-
sendClaudeSessionMessage: () => {
|
|
350
|
-
},
|
|
351
|
-
keepAlive: () => {
|
|
352
|
-
},
|
|
353
|
-
sendSessionEvent: () => {
|
|
354
|
-
},
|
|
355
|
-
sendSessionDeath: () => {
|
|
356
|
-
},
|
|
357
|
-
updateLifecycleState: () => {
|
|
358
|
-
},
|
|
359
|
-
requestControlTransfer: async () => {
|
|
360
|
-
},
|
|
361
|
-
flush: async () => {
|
|
362
|
-
},
|
|
363
|
-
close: async () => {
|
|
364
|
-
},
|
|
365
|
-
updateMetadata: () => {
|
|
366
|
-
},
|
|
367
|
-
updateAgentState: () => {
|
|
368
|
-
},
|
|
369
|
-
onUserMessage: () => {
|
|
370
|
-
},
|
|
371
|
-
rpcHandlerManager: {
|
|
372
|
-
registerHandler: () => {
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function setupOfflineReconnection(opts) {
|
|
379
|
-
const { api: api$1, sessionTag, metadata, state, response, onSessionSwap } = opts;
|
|
380
|
-
let session;
|
|
381
|
-
let reconnectionHandle = null;
|
|
382
|
-
if (!response) {
|
|
383
|
-
session = createOfflineSessionStub(sessionTag);
|
|
384
|
-
reconnectionHandle = api.startOfflineReconnection({
|
|
385
|
-
serverUrl: api.configuration.serverUrl,
|
|
386
|
-
onReconnected: async () => {
|
|
387
|
-
const resp = await api$1.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
388
|
-
if (!resp) throw new Error("Server unavailable");
|
|
389
|
-
const realSession = api$1.sessionSyncClient(resp);
|
|
390
|
-
onSessionSwap(realSession);
|
|
391
|
-
return realSession;
|
|
392
|
-
},
|
|
393
|
-
onNotify: (msg) => {
|
|
394
|
-
console.log(msg);
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
return { session, reconnectionHandle, isOffline: true };
|
|
398
|
-
} else {
|
|
399
|
-
session = api$1.sessionSyncClient(response);
|
|
400
|
-
return { session, reconnectionHandle: null, isOffline: false };
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
532
|
exports.BasePermissionHandler = BasePermissionHandler;
|
|
405
533
|
exports.BaseReasoningProcessor = BaseReasoningProcessor;
|
|
534
|
+
exports.INTERACTION_SUPERSEDED_ERROR = INTERACTION_SUPERSEDED_ERROR;
|
|
535
|
+
exports.INTERACTION_TIMED_OUT_ERROR = INTERACTION_TIMED_OUT_ERROR;
|
|
406
536
|
exports.createSessionMetadata = createSessionMetadata;
|
|
537
|
+
exports.getPendingInteractionTimeoutMs = getPendingInteractionTimeoutMs;
|
|
407
538
|
exports.setupOfflineReconnection = setupOfflineReconnection;
|
|
@@ -1,9 +1,121 @@
|
|
|
1
|
-
import { l as logger, c as configuration, p as packageJson, s as startOfflineReconnection } from './types-BXyraW9R.mjs';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
1
|
import os from 'node:os';
|
|
4
2
|
import { resolve } from 'node:path';
|
|
5
|
-
import { p as
|
|
3
|
+
import { c as configuration, p as packageJson, s as startOfflineReconnection, l as logger } from './api-B5Ui8Fw0.mjs';
|
|
4
|
+
import { p as projectPath } from './index-BByhFIIq.mjs';
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
function createSessionMetadata(opts) {
|
|
9
|
+
const state = {
|
|
10
|
+
controlledByUser: false
|
|
11
|
+
};
|
|
12
|
+
const metadata = {
|
|
13
|
+
path: process.cwd(),
|
|
14
|
+
host: os.hostname(),
|
|
15
|
+
version: packageJson.version,
|
|
16
|
+
os: os.platform(),
|
|
17
|
+
machineId: opts.machineId,
|
|
18
|
+
homeDir: os.homedir(),
|
|
19
|
+
happyHomeDir: configuration.happyCloudHomeDir,
|
|
20
|
+
happyLibDir: projectPath(),
|
|
21
|
+
happyToolsDir: resolve(projectPath(), "tools", "unpacked"),
|
|
22
|
+
startedFromDaemon: opts.startedBy === "daemon",
|
|
23
|
+
hostPid: process.pid,
|
|
24
|
+
startedBy: opts.startedBy || "terminal",
|
|
25
|
+
lifecycleState: "running",
|
|
26
|
+
lifecycleStateSince: Date.now(),
|
|
27
|
+
flavor: opts.flavor
|
|
28
|
+
};
|
|
29
|
+
return { state, metadata };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createOfflineSessionStub(sessionTag) {
|
|
33
|
+
const emitter = new EventEmitter();
|
|
34
|
+
let metadata = null;
|
|
35
|
+
let agentState = null;
|
|
36
|
+
const stub = {
|
|
37
|
+
sessionId: `offline-${sessionTag}`,
|
|
38
|
+
sendCodexMessage: () => {
|
|
39
|
+
},
|
|
40
|
+
sendAgentMessage: () => {
|
|
41
|
+
},
|
|
42
|
+
sendClaudeSessionMessage: () => {
|
|
43
|
+
},
|
|
44
|
+
keepAlive: () => {
|
|
45
|
+
},
|
|
46
|
+
sendSessionEvent: () => {
|
|
47
|
+
},
|
|
48
|
+
sendSessionDeath: () => {
|
|
49
|
+
},
|
|
50
|
+
updateLifecycleState: () => {
|
|
51
|
+
},
|
|
52
|
+
requestControlTransfer: async () => {
|
|
53
|
+
},
|
|
54
|
+
flush: async () => {
|
|
55
|
+
},
|
|
56
|
+
close: async () => {
|
|
57
|
+
},
|
|
58
|
+
updateMetadata: (handler) => {
|
|
59
|
+
metadata = handler(metadata || {});
|
|
60
|
+
emitter.emit("metadata-updated", metadata);
|
|
61
|
+
},
|
|
62
|
+
updateAgentState: (handler) => {
|
|
63
|
+
agentState = handler(agentState || {});
|
|
64
|
+
emitter.emit("agent-state-updated", agentState);
|
|
65
|
+
},
|
|
66
|
+
getMetadataSnapshot: () => metadata,
|
|
67
|
+
getAgentStateSnapshot: () => agentState,
|
|
68
|
+
waitForMetadataUpdate: async () => metadata,
|
|
69
|
+
onUserMessage: () => {
|
|
70
|
+
},
|
|
71
|
+
rpcHandlerManager: {
|
|
72
|
+
registerHandler: () => {
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
on: emitter.on.bind(emitter),
|
|
76
|
+
once: emitter.once.bind(emitter),
|
|
77
|
+
off: emitter.off.bind(emitter),
|
|
78
|
+
emit: emitter.emit.bind(emitter)
|
|
79
|
+
};
|
|
80
|
+
return stub;
|
|
81
|
+
}
|
|
6
82
|
|
|
83
|
+
function setupOfflineReconnection(opts) {
|
|
84
|
+
const { api, sessionTag, metadata, state, response, onSessionSwap } = opts;
|
|
85
|
+
let session;
|
|
86
|
+
let reconnectionHandle = null;
|
|
87
|
+
if (!response) {
|
|
88
|
+
session = createOfflineSessionStub(sessionTag);
|
|
89
|
+
reconnectionHandle = startOfflineReconnection({
|
|
90
|
+
serverUrl: configuration.serverUrl,
|
|
91
|
+
onReconnected: async () => {
|
|
92
|
+
const resp = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
93
|
+
if (!resp) throw new Error("Server unavailable");
|
|
94
|
+
const realSession = api.sessionSyncClient(resp);
|
|
95
|
+
onSessionSwap(realSession);
|
|
96
|
+
return realSession;
|
|
97
|
+
},
|
|
98
|
+
onNotify: (msg) => {
|
|
99
|
+
console.log(msg);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return { session, reconnectionHandle, isOffline: true };
|
|
103
|
+
} else {
|
|
104
|
+
session = api.sessionSyncClient(response);
|
|
105
|
+
return { session, reconnectionHandle: null, isOffline: false };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const INTERACTION_SUPERSEDED_ERROR = "Interaction superseded by new user message";
|
|
110
|
+
const INTERACTION_TIMED_OUT_ERROR = "Interaction timed out waiting for user response";
|
|
111
|
+
const DEFAULT_INTERACTION_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
112
|
+
function getPendingInteractionTimeoutMs() {
|
|
113
|
+
const raw = Number(process.env.HAPPY_INTERACTION_TIMEOUT_MS);
|
|
114
|
+
if (Number.isFinite(raw) && raw > 0) {
|
|
115
|
+
return raw;
|
|
116
|
+
}
|
|
117
|
+
return DEFAULT_INTERACTION_TIMEOUT_MS;
|
|
118
|
+
}
|
|
7
119
|
class BasePermissionHandler {
|
|
8
120
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
9
121
|
session;
|
|
@@ -34,6 +146,7 @@ class BasePermissionHandler {
|
|
|
34
146
|
return;
|
|
35
147
|
}
|
|
36
148
|
this.pendingRequests.delete(response.id);
|
|
149
|
+
this.clearPendingRequestTimeout(pending);
|
|
37
150
|
const result = response.approved ? { decision: response.decision === "approved_for_session" ? "approved_for_session" : "approved" } : { decision: response.decision === "denied" ? "denied" : "abort" };
|
|
38
151
|
pending.resolve(result);
|
|
39
152
|
this.session.updateAgentState((currentState) => {
|
|
@@ -75,6 +188,62 @@ class BasePermissionHandler {
|
|
|
75
188
|
}
|
|
76
189
|
}));
|
|
77
190
|
}
|
|
191
|
+
registerPendingRequest(toolCallId, toolName, input, logSuffix = "") {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const pending = {
|
|
194
|
+
resolve,
|
|
195
|
+
reject,
|
|
196
|
+
toolName,
|
|
197
|
+
input
|
|
198
|
+
};
|
|
199
|
+
pending.timeoutHandle = setTimeout(() => {
|
|
200
|
+
this.handlePendingRequestTimeout(toolCallId, pending);
|
|
201
|
+
}, getPendingInteractionTimeoutMs());
|
|
202
|
+
this.pendingRequests.set(toolCallId, pending);
|
|
203
|
+
this.addPendingRequestToState(toolCallId, toolName, input);
|
|
204
|
+
logger.debug(`${this.getLogPrefix()} Permission request sent for tool: ${toolName} (${toolCallId})${logSuffix}`);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
hasPendingRequests() {
|
|
208
|
+
return this.pendingRequests.size > 0;
|
|
209
|
+
}
|
|
210
|
+
supersedePendingRequests(reason = INTERACTION_SUPERSEDED_ERROR) {
|
|
211
|
+
const pendingSnapshot = Array.from(this.pendingRequests.entries());
|
|
212
|
+
if (pendingSnapshot.length === 0) {
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
this.pendingRequests.clear();
|
|
216
|
+
const completedAt = Date.now();
|
|
217
|
+
for (const [, pending] of pendingSnapshot) {
|
|
218
|
+
this.clearPendingRequestTimeout(pending);
|
|
219
|
+
pending.resolve({ decision: "abort" });
|
|
220
|
+
}
|
|
221
|
+
this.session.updateAgentState((currentState) => {
|
|
222
|
+
const requests = { ...currentState.requests || {} };
|
|
223
|
+
const completedRequests = { ...currentState.completedRequests || {} };
|
|
224
|
+
for (const [id, request] of Object.entries(requests)) {
|
|
225
|
+
if (request.requestKind === "selection") {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
completedRequests[id] = {
|
|
229
|
+
...request,
|
|
230
|
+
completedAt,
|
|
231
|
+
status: "denied",
|
|
232
|
+
reason,
|
|
233
|
+
decision: "abort",
|
|
234
|
+
requestKind: request.requestKind || "permission"
|
|
235
|
+
};
|
|
236
|
+
delete requests[id];
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
...currentState,
|
|
240
|
+
requests,
|
|
241
|
+
completedRequests
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
logger.debug(`${this.getLogPrefix()} Superseded ${pendingSnapshot.length} pending permission request(s)`);
|
|
245
|
+
return pendingSnapshot.length;
|
|
246
|
+
}
|
|
78
247
|
/**
|
|
79
248
|
* Reset state for new sessions.
|
|
80
249
|
* This method is idempotent - safe to call multiple times.
|
|
@@ -90,6 +259,7 @@ class BasePermissionHandler {
|
|
|
90
259
|
this.pendingRequests.clear();
|
|
91
260
|
for (const [id, pending] of pendingSnapshot) {
|
|
92
261
|
try {
|
|
262
|
+
this.clearPendingRequestTimeout(pending);
|
|
93
263
|
pending.reject(new Error("Session reset"));
|
|
94
264
|
} catch (err) {
|
|
95
265
|
logger.debug(`${this.getLogPrefix()} Error rejecting pending request ${id}:`, err);
|
|
@@ -117,6 +287,50 @@ class BasePermissionHandler {
|
|
|
117
287
|
this.isResetting = false;
|
|
118
288
|
}
|
|
119
289
|
}
|
|
290
|
+
clearPendingRequestTimeout(pending) {
|
|
291
|
+
if (pending?.timeoutHandle) {
|
|
292
|
+
clearTimeout(pending.timeoutHandle);
|
|
293
|
+
pending.timeoutHandle = void 0;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
handlePendingRequestTimeout(toolCallId, pending) {
|
|
297
|
+
const active = this.pendingRequests.get(toolCallId);
|
|
298
|
+
if (!active || active !== pending) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
this.pendingRequests.delete(toolCallId);
|
|
302
|
+
this.clearPendingRequestTimeout(active);
|
|
303
|
+
active.resolve({ decision: "abort" });
|
|
304
|
+
this.session.updateAgentState((currentState) => {
|
|
305
|
+
const request = currentState.requests?.[toolCallId] || {
|
|
306
|
+
tool: active.toolName,
|
|
307
|
+
arguments: active.input,
|
|
308
|
+
createdAt: Date.now(),
|
|
309
|
+
requestKind: "permission"
|
|
310
|
+
};
|
|
311
|
+
const { [toolCallId]: _, ...remainingRequests } = currentState.requests || {};
|
|
312
|
+
return {
|
|
313
|
+
...currentState,
|
|
314
|
+
requests: remainingRequests,
|
|
315
|
+
completedRequests: {
|
|
316
|
+
...currentState.completedRequests,
|
|
317
|
+
[toolCallId]: {
|
|
318
|
+
...request,
|
|
319
|
+
completedAt: Date.now(),
|
|
320
|
+
status: "canceled",
|
|
321
|
+
reason: INTERACTION_TIMED_OUT_ERROR,
|
|
322
|
+
decision: "abort",
|
|
323
|
+
requestKind: request.requestKind || "permission"
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
this.session.sendSessionEvent({
|
|
329
|
+
type: "message",
|
|
330
|
+
message: "Pending interaction timed out waiting for a response. Send a new message to continue."
|
|
331
|
+
});
|
|
332
|
+
logger.debug(`${this.getLogPrefix()} Permission request timed out for ${active.toolName} (${toolCallId})`);
|
|
333
|
+
}
|
|
120
334
|
}
|
|
121
335
|
|
|
122
336
|
class BaseReasoningProcessor {
|
|
@@ -313,90 +527,4 @@ class BaseReasoningProcessor {
|
|
|
313
527
|
}
|
|
314
528
|
}
|
|
315
529
|
|
|
316
|
-
|
|
317
|
-
const state = {
|
|
318
|
-
controlledByUser: false
|
|
319
|
-
};
|
|
320
|
-
const metadata = {
|
|
321
|
-
path: process.cwd(),
|
|
322
|
-
host: os.hostname(),
|
|
323
|
-
version: packageJson.version,
|
|
324
|
-
os: os.platform(),
|
|
325
|
-
machineId: opts.machineId,
|
|
326
|
-
homeDir: os.homedir(),
|
|
327
|
-
happyHomeDir: configuration.happyCloudHomeDir,
|
|
328
|
-
happyLibDir: projectPath(),
|
|
329
|
-
happyToolsDir: resolve(projectPath(), "tools", "unpacked"),
|
|
330
|
-
startedFromDaemon: opts.startedBy === "daemon",
|
|
331
|
-
hostPid: process.pid,
|
|
332
|
-
startedBy: opts.startedBy || "terminal",
|
|
333
|
-
lifecycleState: "running",
|
|
334
|
-
lifecycleStateSince: Date.now(),
|
|
335
|
-
flavor: opts.flavor
|
|
336
|
-
};
|
|
337
|
-
return { state, metadata };
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function createOfflineSessionStub(sessionTag) {
|
|
341
|
-
return {
|
|
342
|
-
sessionId: `offline-${sessionTag}`,
|
|
343
|
-
sendCodexMessage: () => {
|
|
344
|
-
},
|
|
345
|
-
sendAgentMessage: () => {
|
|
346
|
-
},
|
|
347
|
-
sendClaudeSessionMessage: () => {
|
|
348
|
-
},
|
|
349
|
-
keepAlive: () => {
|
|
350
|
-
},
|
|
351
|
-
sendSessionEvent: () => {
|
|
352
|
-
},
|
|
353
|
-
sendSessionDeath: () => {
|
|
354
|
-
},
|
|
355
|
-
updateLifecycleState: () => {
|
|
356
|
-
},
|
|
357
|
-
requestControlTransfer: async () => {
|
|
358
|
-
},
|
|
359
|
-
flush: async () => {
|
|
360
|
-
},
|
|
361
|
-
close: async () => {
|
|
362
|
-
},
|
|
363
|
-
updateMetadata: () => {
|
|
364
|
-
},
|
|
365
|
-
updateAgentState: () => {
|
|
366
|
-
},
|
|
367
|
-
onUserMessage: () => {
|
|
368
|
-
},
|
|
369
|
-
rpcHandlerManager: {
|
|
370
|
-
registerHandler: () => {
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function setupOfflineReconnection(opts) {
|
|
377
|
-
const { api, sessionTag, metadata, state, response, onSessionSwap } = opts;
|
|
378
|
-
let session;
|
|
379
|
-
let reconnectionHandle = null;
|
|
380
|
-
if (!response) {
|
|
381
|
-
session = createOfflineSessionStub(sessionTag);
|
|
382
|
-
reconnectionHandle = startOfflineReconnection({
|
|
383
|
-
serverUrl: configuration.serverUrl,
|
|
384
|
-
onReconnected: async () => {
|
|
385
|
-
const resp = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
386
|
-
if (!resp) throw new Error("Server unavailable");
|
|
387
|
-
const realSession = api.sessionSyncClient(resp);
|
|
388
|
-
onSessionSwap(realSession);
|
|
389
|
-
return realSession;
|
|
390
|
-
},
|
|
391
|
-
onNotify: (msg) => {
|
|
392
|
-
console.log(msg);
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
return { session, reconnectionHandle, isOffline: true };
|
|
396
|
-
} else {
|
|
397
|
-
session = api.sessionSyncClient(response);
|
|
398
|
-
return { session, reconnectionHandle: null, isOffline: false };
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
export { BasePermissionHandler as B, BaseReasoningProcessor as a, createSessionMetadata as c, setupOfflineReconnection as s };
|
|
530
|
+
export { BasePermissionHandler as B, INTERACTION_SUPERSEDED_ERROR as I, BaseReasoningProcessor as a, INTERACTION_TIMED_OUT_ERROR as b, createSessionMetadata as c, getPendingInteractionTimeoutMs as g, setupOfflineReconnection as s };
|