@ouro.bot/cli 0.1.0-alpha.803 → 0.1.0-alpha.805
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/changelog.json +14 -0
- package/deploy/unraid/README.txt +38 -18
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/core.js +14 -11
- package/dist/heart/daemon/cli-defaults.js +22 -4
- package/dist/heart/daemon/cli-exec.js +95 -6
- package/dist/heart/daemon/cli-help.js +6 -0
- package/dist/heart/daemon/cli-parse.js +52 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +8 -0
- package/dist/heart/daemon/daemon-entry.js +2 -1
- package/dist/heart/daemon/daemon.js +94 -47
- package/dist/heart/daemon/sense-manager.js +1 -1
- package/dist/heart/frontend-approval-runtime.js +432 -0
- package/dist/heart/frontend-journal.js +215 -0
- package/dist/heart/frontend-session-service.js +237 -0
- package/dist/heart/frontend-socket-client.js +154 -0
- package/dist/heart/frontend-socket.js +400 -0
- package/dist/heart/mail-import-discovery.js +3 -0
- package/dist/heart/turn-context.js +1 -0
- package/dist/heart/turn-execution-lease.js +30 -0
- package/dist/mind/prompt.js +4 -1
- package/dist/repertoire/mcp-manager.js +82 -17
- package/dist/senses/acp-server.js +386 -0
- package/dist/senses/pipeline.js +5 -0
- package/dist/senses/shared-turn.js +369 -257
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FrontendJournalStore = exports.FrontendJournalPayloadTooLargeError = exports.FrontendJournalCorruptError = void 0;
|
|
37
|
+
const node_crypto_1 = require("node:crypto");
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const identity_1 = require("./identity");
|
|
41
|
+
const runtime_1 = require("../nerves/runtime");
|
|
42
|
+
const JOURNAL_VERSION = 1;
|
|
43
|
+
const DEFAULT_MAX_EVENT_BYTES = 256 * 1024;
|
|
44
|
+
const DEFAULT_REPLAY_LIMIT = 1_000;
|
|
45
|
+
const EVENT_TYPES = new Set([
|
|
46
|
+
"user_message",
|
|
47
|
+
"turn_started",
|
|
48
|
+
"assistant_delivery",
|
|
49
|
+
"tool_started",
|
|
50
|
+
"tool_completed",
|
|
51
|
+
"structured_output",
|
|
52
|
+
"permission_requested",
|
|
53
|
+
"permission_resolved",
|
|
54
|
+
"error",
|
|
55
|
+
"turn_completed",
|
|
56
|
+
"turn_failed",
|
|
57
|
+
"turn_cancelled",
|
|
58
|
+
]);
|
|
59
|
+
class FrontendJournalCorruptError extends Error {
|
|
60
|
+
constructor(journalPath) {
|
|
61
|
+
super(`frontend journal has an invalid tail: ${journalPath}`);
|
|
62
|
+
this.name = "FrontendJournalCorruptError";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.FrontendJournalCorruptError = FrontendJournalCorruptError;
|
|
66
|
+
class FrontendJournalPayloadTooLargeError extends Error {
|
|
67
|
+
constructor(size, limit) {
|
|
68
|
+
super(`frontend journal event is ${size} bytes; limit is ${limit}`);
|
|
69
|
+
this.name = "FrontendJournalPayloadTooLargeError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.FrontendJournalPayloadTooLargeError = FrontendJournalPayloadTooLargeError;
|
|
73
|
+
function required(value, field) {
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
if (!trimmed)
|
|
76
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
77
|
+
return trimmed;
|
|
78
|
+
}
|
|
79
|
+
function opaqueSegment(value) {
|
|
80
|
+
return (0, node_crypto_1.createHash)("sha256").update(value).digest("hex").slice(0, 32);
|
|
81
|
+
}
|
|
82
|
+
function record(value) {
|
|
83
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
84
|
+
throw new Error("journal data must be an object");
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
function parseEvent(raw, ref, expectedSequence) {
|
|
88
|
+
const value = record(raw);
|
|
89
|
+
if (value.version !== JOURNAL_VERSION)
|
|
90
|
+
throw new Error("unsupported journal version");
|
|
91
|
+
if (value.sequence !== expectedSequence)
|
|
92
|
+
throw new Error("invalid journal sequence");
|
|
93
|
+
if (value.agent !== ref.agent || value.friendId !== ref.friendId || value.sessionId !== ref.sessionId) {
|
|
94
|
+
throw new Error("journal identity mismatch");
|
|
95
|
+
}
|
|
96
|
+
if (typeof value.turnId !== "string" || !value.turnId)
|
|
97
|
+
throw new Error("invalid journal turnId");
|
|
98
|
+
if (typeof value.type !== "string" || !EVENT_TYPES.has(value.type)) {
|
|
99
|
+
throw new Error("invalid journal event type");
|
|
100
|
+
}
|
|
101
|
+
if (typeof value.occurredAt !== "string" || !value.occurredAt)
|
|
102
|
+
throw new Error("invalid journal timestamp");
|
|
103
|
+
return {
|
|
104
|
+
version: JOURNAL_VERSION,
|
|
105
|
+
sequence: expectedSequence,
|
|
106
|
+
agent: ref.agent,
|
|
107
|
+
friendId: ref.friendId,
|
|
108
|
+
sessionId: ref.sessionId,
|
|
109
|
+
turnId: value.turnId,
|
|
110
|
+
type: value.type,
|
|
111
|
+
occurredAt: value.occurredAt,
|
|
112
|
+
data: record(value.data),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
class FrontendJournalStore {
|
|
116
|
+
agentRoot;
|
|
117
|
+
now;
|
|
118
|
+
maxEventBytes;
|
|
119
|
+
constructor(options = {}) {
|
|
120
|
+
this.agentRoot = options.agentRoot ?? identity_1.getAgentRoot;
|
|
121
|
+
this.now = options.now ?? (() => new Date().toISOString());
|
|
122
|
+
this.maxEventBytes = options.maxEventBytes ?? DEFAULT_MAX_EVENT_BYTES;
|
|
123
|
+
if (!Number.isSafeInteger(this.maxEventBytes) || this.maxEventBytes < 1) {
|
|
124
|
+
throw new Error("maxEventBytes must be a positive integer");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
pathFor(ref) {
|
|
128
|
+
const agent = required(ref.agent, "agent");
|
|
129
|
+
const friendId = required(ref.friendId, "friendId");
|
|
130
|
+
const sessionId = required(ref.sessionId, "sessionId");
|
|
131
|
+
return path.join(this.agentRoot(agent), "state", "frontend-sessions", opaqueSegment(friendId), `${opaqueSegment(sessionId)}.jsonl`);
|
|
132
|
+
}
|
|
133
|
+
replay(ref, options = {}) {
|
|
134
|
+
const afterSequence = options.afterSequence ?? 0;
|
|
135
|
+
const limit = options.limit ?? DEFAULT_REPLAY_LIMIT;
|
|
136
|
+
if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) {
|
|
137
|
+
throw new Error("afterSequence must be a non-negative integer");
|
|
138
|
+
}
|
|
139
|
+
if (!Number.isSafeInteger(limit) || limit < 1) {
|
|
140
|
+
throw new Error("limit must be a positive integer");
|
|
141
|
+
}
|
|
142
|
+
const journalPath = this.pathFor(ref);
|
|
143
|
+
if (!fs.existsSync(journalPath)) {
|
|
144
|
+
return { events: [], lastSequence: 0, hasMore: false, degraded: false, incompleteTurnIds: [] };
|
|
145
|
+
}
|
|
146
|
+
const valid = [];
|
|
147
|
+
let degraded = false;
|
|
148
|
+
const lines = fs.readFileSync(journalPath, "utf8").split("\n").filter((line) => line.length > 0);
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
try {
|
|
151
|
+
valid.push(parseEvent(JSON.parse(line), ref, valid.length + 1));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
degraded = true;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const matching = valid.filter((event) => event.sequence > afterSequence);
|
|
159
|
+
const incompleteTurnIds = new Set();
|
|
160
|
+
for (const event of valid) {
|
|
161
|
+
if (event.type === "turn_started") {
|
|
162
|
+
incompleteTurnIds.add(event.turnId);
|
|
163
|
+
}
|
|
164
|
+
else if (event.type === "turn_completed"
|
|
165
|
+
|| event.type === "turn_failed"
|
|
166
|
+
|| event.type === "turn_cancelled") {
|
|
167
|
+
incompleteTurnIds.delete(event.turnId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
events: matching.slice(0, limit),
|
|
172
|
+
lastSequence: valid.at(-1)?.sequence ?? 0,
|
|
173
|
+
hasMore: matching.length > limit,
|
|
174
|
+
degraded,
|
|
175
|
+
incompleteTurnIds: [...incompleteTurnIds],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
append(ref, input) {
|
|
179
|
+
const journalPath = this.pathFor(ref);
|
|
180
|
+
const replay = this.replay(ref);
|
|
181
|
+
if (replay.degraded)
|
|
182
|
+
throw new FrontendJournalCorruptError(journalPath);
|
|
183
|
+
if (!EVENT_TYPES.has(input.type))
|
|
184
|
+
throw new Error("invalid journal event type");
|
|
185
|
+
const event = {
|
|
186
|
+
version: JOURNAL_VERSION,
|
|
187
|
+
sequence: replay.lastSequence + 1,
|
|
188
|
+
agent: required(ref.agent, "agent"),
|
|
189
|
+
friendId: required(ref.friendId, "friendId"),
|
|
190
|
+
sessionId: required(ref.sessionId, "sessionId"),
|
|
191
|
+
turnId: required(input.turnId, "turnId"),
|
|
192
|
+
type: input.type,
|
|
193
|
+
occurredAt: this.now(),
|
|
194
|
+
data: record(input.data),
|
|
195
|
+
};
|
|
196
|
+
const encoded = `${JSON.stringify(event)}\n`;
|
|
197
|
+
const size = Buffer.byteLength(encoded);
|
|
198
|
+
if (size > this.maxEventBytes)
|
|
199
|
+
throw new FrontendJournalPayloadTooLargeError(size, this.maxEventBytes);
|
|
200
|
+
// ponytail: semantic event volume is low; add a side index only if replay-on-append becomes measurable.
|
|
201
|
+
const directory = path.dirname(journalPath);
|
|
202
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
203
|
+
fs.chmodSync(directory, 0o700);
|
|
204
|
+
fs.appendFileSync(journalPath, encoded, { encoding: "utf8", mode: 0o600 });
|
|
205
|
+
fs.chmodSync(journalPath, 0o600);
|
|
206
|
+
(0, runtime_1.emitNervesEvent)({
|
|
207
|
+
component: "heart",
|
|
208
|
+
event: "heart.frontend_journal_appended",
|
|
209
|
+
message: "frontend journal event appended",
|
|
210
|
+
meta: { type: event.type, sequence: event.sequence },
|
|
211
|
+
});
|
|
212
|
+
return event;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
exports.FrontendJournalStore = FrontendJournalStore;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FrontendSessionService = exports.FrontendSessionConflictError = exports.FrontendTurnConflictError = void 0;
|
|
4
|
+
const shared_turn_1 = require("../senses/shared-turn");
|
|
5
|
+
const runtime_1 = require("../nerves/runtime");
|
|
6
|
+
class FrontendTurnConflictError extends Error {
|
|
7
|
+
constructor(turnId) {
|
|
8
|
+
super(`frontend turn already exists: ${turnId}`);
|
|
9
|
+
this.name = "FrontendTurnConflictError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.FrontendTurnConflictError = FrontendTurnConflictError;
|
|
13
|
+
class FrontendSessionConflictError extends Error {
|
|
14
|
+
constructor(sessionKey) {
|
|
15
|
+
super(`frontend session already has an active turn: ${sessionKey}`);
|
|
16
|
+
this.name = "FrontendSessionConflictError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.FrontendSessionConflictError = FrontendSessionConflictError;
|
|
20
|
+
const JOURNALED_FRONTEND_EVENTS = new Set([
|
|
21
|
+
"assistant_delivery",
|
|
22
|
+
"tool_started",
|
|
23
|
+
"tool_completed",
|
|
24
|
+
"structured_output",
|
|
25
|
+
"error",
|
|
26
|
+
]);
|
|
27
|
+
function required(value, field) {
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (!trimmed)
|
|
30
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
31
|
+
return trimmed;
|
|
32
|
+
}
|
|
33
|
+
class FrontendSessionService {
|
|
34
|
+
activeTurns = new Map();
|
|
35
|
+
activeSessions = new Map();
|
|
36
|
+
listeners = new Set();
|
|
37
|
+
runner;
|
|
38
|
+
journal;
|
|
39
|
+
authority;
|
|
40
|
+
closed = false;
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
this.runner = options.runner ?? shared_turn_1.runSenseTurn;
|
|
43
|
+
this.journal = options.journal ?? null;
|
|
44
|
+
this.authority = options.authority ?? null;
|
|
45
|
+
}
|
|
46
|
+
hasTurn(turnId) {
|
|
47
|
+
return this.activeTurns.has(turnId);
|
|
48
|
+
}
|
|
49
|
+
cancelTurn(turnId) {
|
|
50
|
+
const controller = this.activeTurns.get(turnId);
|
|
51
|
+
if (!controller || controller.signal.aborted)
|
|
52
|
+
return false;
|
|
53
|
+
controller.abort();
|
|
54
|
+
this.authority?.cancelTurn(turnId);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
cancelAllTurns() {
|
|
58
|
+
let cancelled = 0;
|
|
59
|
+
for (const controller of this.activeTurns.values()) {
|
|
60
|
+
if (controller.signal.aborted)
|
|
61
|
+
continue;
|
|
62
|
+
controller.abort();
|
|
63
|
+
cancelled += 1;
|
|
64
|
+
}
|
|
65
|
+
return cancelled;
|
|
66
|
+
}
|
|
67
|
+
subscribe(listener) {
|
|
68
|
+
this.listeners.add(listener);
|
|
69
|
+
return () => this.listeners.delete(listener);
|
|
70
|
+
}
|
|
71
|
+
resolvePermission(requestId, optionId) {
|
|
72
|
+
return this.authority?.resolvePermission(required(requestId, "requestId"), required(optionId, "optionId")) ?? false;
|
|
73
|
+
}
|
|
74
|
+
close() {
|
|
75
|
+
if (this.closed)
|
|
76
|
+
return;
|
|
77
|
+
this.closed = true;
|
|
78
|
+
this.authority?.close();
|
|
79
|
+
}
|
|
80
|
+
loadSession(ref, options = {}) {
|
|
81
|
+
if (!this.journal)
|
|
82
|
+
throw new Error("frontend journal is unavailable");
|
|
83
|
+
const normalized = {
|
|
84
|
+
agent: required(ref.agent, "agent"),
|
|
85
|
+
friendId: required(ref.friendId, "friendId"),
|
|
86
|
+
sessionId: required(ref.sessionKey, "sessionKey"),
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
...this.journal.replay(normalized, options),
|
|
90
|
+
pendingPermissions: this.authority?.pendingPermissions({
|
|
91
|
+
agent: normalized.agent,
|
|
92
|
+
friendId: normalized.friendId,
|
|
93
|
+
sessionKey: normalized.sessionId,
|
|
94
|
+
}) ?? [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
prepareTurn(request) {
|
|
98
|
+
const turnId = required(request.turnId, "turnId");
|
|
99
|
+
const normalized = {
|
|
100
|
+
...request,
|
|
101
|
+
turnId,
|
|
102
|
+
agent: required(request.agent, "agent"),
|
|
103
|
+
friendId: required(request.friendId, "friendId"),
|
|
104
|
+
sessionKey: required(request.sessionKey, "sessionKey"),
|
|
105
|
+
message: required(request.message, "message"),
|
|
106
|
+
};
|
|
107
|
+
if (this.activeTurns.has(turnId))
|
|
108
|
+
throw new FrontendTurnConflictError(turnId);
|
|
109
|
+
const sessionIdentity = [normalized.agent, normalized.friendId, normalized.channel, normalized.sessionKey].join("\0");
|
|
110
|
+
if (this.activeSessions.has(sessionIdentity))
|
|
111
|
+
throw new FrontendSessionConflictError(normalized.sessionKey);
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
this.activeTurns.set(turnId, controller);
|
|
114
|
+
this.activeSessions.set(sessionIdentity, turnId);
|
|
115
|
+
return { request: normalized, sessionIdentity, controller, started: false };
|
|
116
|
+
}
|
|
117
|
+
async runTurn(request) {
|
|
118
|
+
return this.runPreparedTurn(this.prepareTurn(request));
|
|
119
|
+
}
|
|
120
|
+
async runPreparedTurn(prepared) {
|
|
121
|
+
if (prepared.started)
|
|
122
|
+
throw new Error(`frontend turn already started: ${prepared.request.turnId}`);
|
|
123
|
+
prepared.started = true;
|
|
124
|
+
const normalized = prepared.request;
|
|
125
|
+
const turnId = normalized.turnId;
|
|
126
|
+
const sessionIdentity = prepared.sessionIdentity;
|
|
127
|
+
const controller = prepared.controller;
|
|
128
|
+
try {
|
|
129
|
+
this.publish(normalized, "user_message", { text: normalized.message }, "user_message");
|
|
130
|
+
this.publish(normalized, "turn_started", {}, "turn_started");
|
|
131
|
+
const frontendEventSink = {
|
|
132
|
+
onEvent: (event) => this.publishFrontendEvent(normalized, event),
|
|
133
|
+
};
|
|
134
|
+
const approvalCoordinatorFactory = normalized.ephemeral
|
|
135
|
+
? undefined
|
|
136
|
+
: this.authority?.approvalCoordinatorFactory({
|
|
137
|
+
request: normalized,
|
|
138
|
+
publish: (type, data, journalType) => this.publish(normalized, type, data, journalType),
|
|
139
|
+
});
|
|
140
|
+
let result = await this.runner({
|
|
141
|
+
agentName: normalized.agent,
|
|
142
|
+
friendId: normalized.friendId,
|
|
143
|
+
channel: normalized.channel,
|
|
144
|
+
sessionKey: normalized.sessionKey,
|
|
145
|
+
userMessage: normalized.message,
|
|
146
|
+
signal: controller.signal,
|
|
147
|
+
latencyMode: "live",
|
|
148
|
+
frontendEventSink,
|
|
149
|
+
...(normalized.disableTools ? { disableTools: true } : {}),
|
|
150
|
+
...(normalized.ephemeral ? { disablePersistence: true } : {}),
|
|
151
|
+
...(approvalCoordinatorFactory ? { approvalCoordinatorFactory } : {}),
|
|
152
|
+
...(normalized.runtimeMcpServers ? { runtimeMcpServers: normalized.runtimeMcpServers } : {}),
|
|
153
|
+
});
|
|
154
|
+
let suspensionRounds = 0;
|
|
155
|
+
while (result.turnOutcome === "suspended") {
|
|
156
|
+
if (!result.suspension)
|
|
157
|
+
throw new Error(`frontend turn ${turnId} omitted its approval suspension`);
|
|
158
|
+
if (!this.authority)
|
|
159
|
+
throw new Error(`frontend turn ${turnId} suspended without an authority runtime`);
|
|
160
|
+
suspensionRounds += 1;
|
|
161
|
+
if (suspensionRounds > 8)
|
|
162
|
+
throw new Error(`frontend turn ${turnId} exceeded its approval suspension limit`);
|
|
163
|
+
result = await this.authority.resumeApproval({
|
|
164
|
+
request: normalized,
|
|
165
|
+
suspension: result.suspension,
|
|
166
|
+
signal: controller.signal,
|
|
167
|
+
frontendEventSink,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!result.turnOutcome)
|
|
171
|
+
throw new Error(`frontend turn ${turnId} omitted its outcome`);
|
|
172
|
+
const frontendResult = {
|
|
173
|
+
turnId,
|
|
174
|
+
outcome: result.turnOutcome,
|
|
175
|
+
response: result.response,
|
|
176
|
+
sessionPath: result.sessionPath ?? null,
|
|
177
|
+
};
|
|
178
|
+
const terminalType = result.turnOutcome === "aborted"
|
|
179
|
+
? "turn_cancelled"
|
|
180
|
+
: result.turnOutcome === "errored"
|
|
181
|
+
? "turn_failed"
|
|
182
|
+
: "turn_completed";
|
|
183
|
+
this.publish(normalized, terminalType, { result: frontendResult }, terminalType);
|
|
184
|
+
return frontendResult;
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
this.publish(normalized, "turn_failed", {
|
|
188
|
+
error: error instanceof Error ? error.message : String(error),
|
|
189
|
+
}, "turn_failed");
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
this.activeTurns.delete(turnId);
|
|
194
|
+
this.activeSessions.delete(sessionIdentity);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
publishFrontendEvent(request, event) {
|
|
198
|
+
const journalType = JOURNALED_FRONTEND_EVENTS.has(event.type)
|
|
199
|
+
? event.type
|
|
200
|
+
: undefined;
|
|
201
|
+
this.publish(request, event.type, event.data, journalType);
|
|
202
|
+
}
|
|
203
|
+
publish(request, type, data, journalType) {
|
|
204
|
+
const journalRef = {
|
|
205
|
+
agent: request.agent,
|
|
206
|
+
friendId: request.friendId,
|
|
207
|
+
sessionId: request.sessionKey,
|
|
208
|
+
};
|
|
209
|
+
const journalSequence = !request.ephemeral && this.journal && journalType
|
|
210
|
+
? this.journal.append(journalRef, { turnId: request.turnId, type: journalType, data }).sequence
|
|
211
|
+
: null;
|
|
212
|
+
const event = {
|
|
213
|
+
...journalRef,
|
|
214
|
+
sessionKey: journalRef.sessionId,
|
|
215
|
+
turnId: request.turnId,
|
|
216
|
+
type,
|
|
217
|
+
data,
|
|
218
|
+
journalSequence,
|
|
219
|
+
ephemeral: request.ephemeral === true,
|
|
220
|
+
};
|
|
221
|
+
for (const listener of this.listeners) {
|
|
222
|
+
try {
|
|
223
|
+
listener(event);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
(0, runtime_1.emitNervesEvent)({
|
|
227
|
+
level: "warn",
|
|
228
|
+
component: "daemon",
|
|
229
|
+
event: "daemon.frontend_listener_error",
|
|
230
|
+
message: "frontend session listener failed",
|
|
231
|
+
meta: { type, error: error instanceof Error ? error.message : String(error) },
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
exports.FrontendSessionService = FrontendSessionService;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SocketFrontendClient = void 0;
|
|
37
|
+
const net = __importStar(require("node:net"));
|
|
38
|
+
const runtime_1 = require("../nerves/runtime");
|
|
39
|
+
class SocketFrontendClient {
|
|
40
|
+
socketPath;
|
|
41
|
+
pending = new Map();
|
|
42
|
+
listeners = new Set();
|
|
43
|
+
closeListeners = new Set();
|
|
44
|
+
socket = null;
|
|
45
|
+
connecting = null;
|
|
46
|
+
nextId = 1;
|
|
47
|
+
buffer = "";
|
|
48
|
+
constructor(socketPath) {
|
|
49
|
+
this.socketPath = socketPath;
|
|
50
|
+
}
|
|
51
|
+
async request(method, params) {
|
|
52
|
+
await this.connect();
|
|
53
|
+
const id = String(this.nextId++);
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
this.pending.set(id, { resolve, reject });
|
|
56
|
+
this.socket.write(`${JSON.stringify({ protocolVersion: 1, id, method, params })}\n`);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
onEvent(listener) {
|
|
60
|
+
this.listeners.add(listener);
|
|
61
|
+
return () => this.listeners.delete(listener);
|
|
62
|
+
}
|
|
63
|
+
onClose(listener) {
|
|
64
|
+
this.closeListeners.add(listener);
|
|
65
|
+
return () => this.closeListeners.delete(listener);
|
|
66
|
+
}
|
|
67
|
+
close() {
|
|
68
|
+
this.failPending(new Error("frontend socket closed"));
|
|
69
|
+
this.socket?.destroy();
|
|
70
|
+
this.socket = null;
|
|
71
|
+
}
|
|
72
|
+
connect() {
|
|
73
|
+
if (this.socket && !this.socket.destroyed)
|
|
74
|
+
return Promise.resolve();
|
|
75
|
+
if (this.connecting)
|
|
76
|
+
return this.connecting;
|
|
77
|
+
this.connecting = new Promise((resolve, reject) => {
|
|
78
|
+
const socket = net.createConnection(this.socketPath);
|
|
79
|
+
const fail = (error) => {
|
|
80
|
+
this.connecting = null;
|
|
81
|
+
reject(error);
|
|
82
|
+
};
|
|
83
|
+
socket.once("error", fail);
|
|
84
|
+
socket.once("connect", () => {
|
|
85
|
+
socket.removeListener("error", fail);
|
|
86
|
+
socket.on("error", (error) => {
|
|
87
|
+
this.failPending(error);
|
|
88
|
+
for (const listener of this.closeListeners)
|
|
89
|
+
listener(error);
|
|
90
|
+
});
|
|
91
|
+
socket.on("data", (chunk) => this.handleData(chunk.toString("utf8")));
|
|
92
|
+
socket.on("close", () => {
|
|
93
|
+
const error = new Error("frontend socket closed");
|
|
94
|
+
this.failPending(error);
|
|
95
|
+
for (const listener of this.closeListeners)
|
|
96
|
+
listener(error);
|
|
97
|
+
if (this.socket === socket)
|
|
98
|
+
this.socket = null;
|
|
99
|
+
});
|
|
100
|
+
this.socket = socket;
|
|
101
|
+
this.connecting = null;
|
|
102
|
+
(0, runtime_1.emitNervesEvent)({
|
|
103
|
+
component: "heart",
|
|
104
|
+
event: "heart.frontend_socket_client_connected",
|
|
105
|
+
message: "frontend socket client connected",
|
|
106
|
+
});
|
|
107
|
+
resolve();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
return this.connecting;
|
|
111
|
+
}
|
|
112
|
+
handleData(chunk) {
|
|
113
|
+
this.buffer += chunk;
|
|
114
|
+
for (;;) {
|
|
115
|
+
const newline = this.buffer.indexOf("\n");
|
|
116
|
+
if (newline < 0)
|
|
117
|
+
return;
|
|
118
|
+
const line = this.buffer.slice(0, newline).trim();
|
|
119
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
120
|
+
if (!line)
|
|
121
|
+
continue;
|
|
122
|
+
let frame;
|
|
123
|
+
try {
|
|
124
|
+
frame = JSON.parse(line);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
this.failPending(new Error("invalid frontend socket response"));
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (typeof frame.id === "string") {
|
|
131
|
+
const pending = this.pending.get(frame.id);
|
|
132
|
+
if (!pending)
|
|
133
|
+
continue;
|
|
134
|
+
this.pending.delete(frame.id);
|
|
135
|
+
if (frame.ok === false) {
|
|
136
|
+
pending.reject(new Error(String(frame.error?.message ?? "frontend request failed")));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
pending.resolve(frame.result);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (typeof frame.event === "string") {
|
|
143
|
+
for (const listener of this.listeners)
|
|
144
|
+
listener(frame);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
failPending(error) {
|
|
149
|
+
for (const pending of this.pending.values())
|
|
150
|
+
pending.reject(error);
|
|
151
|
+
this.pending.clear();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
exports.SocketFrontendClient = SocketFrontendClient;
|