@geravant/sinain 1.22.8 → 1.22.9
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/onboard.js
CHANGED
|
@@ -267,15 +267,39 @@ export async function runOnboard(args = {}) {
|
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
270
|
+
// OpenClaw gateway is opt-in: most users run sinain in standalone mode
|
|
271
|
+
// and never need a gateway. Default the prompt to "No" on fresh installs
|
|
272
|
+
// (when no openclaw profile already exists) so accepting all wizard
|
|
273
|
+
// defaults never silently provisions a gateway profile or starts a WS
|
|
274
|
+
// reconnect loop. Mirrors the Quick-path gate at line ~177.
|
|
275
|
+
const hasExistingGateway = (() => {
|
|
276
|
+
try {
|
|
277
|
+
const agentsPath = path.join(SINAIN_DIR, "agents.json");
|
|
278
|
+
if (!fs.existsSync(agentsPath)) return false;
|
|
279
|
+
const cfg = JSON.parse(fs.readFileSync(agentsPath, "utf-8"));
|
|
280
|
+
return !!cfg?.profiles?.openclaw;
|
|
281
|
+
} catch { return false; }
|
|
282
|
+
})();
|
|
283
|
+
const enableGateway = guard(await p.confirm({
|
|
284
|
+
message: "[3/6] Configure OpenClaw gateway?",
|
|
285
|
+
initialValue: hasExistingGateway,
|
|
286
|
+
}));
|
|
287
|
+
if (enableGateway) {
|
|
288
|
+
// stepGateway returns { envVars, agentsPatch }: tokens go to .env,
|
|
289
|
+
// URLs + session + escalation mode go to agents.json's openclaw profile.
|
|
290
|
+
const gatewayResult = await stepGateway(base, "[3/6] OpenClaw gateway");
|
|
291
|
+
Object.assign(vars, gatewayResult.envVars);
|
|
292
|
+
Object.assign(agentsPatch, gatewayResult.agentsPatch);
|
|
293
|
+
if (gatewayResult.agentsPatch.escalationMode === "off") {
|
|
294
|
+
p.log.info("Standalone mode (no gateway).");
|
|
295
|
+
} else {
|
|
296
|
+
p.log.success("Gateway configured.");
|
|
297
|
+
}
|
|
277
298
|
} else {
|
|
278
|
-
|
|
299
|
+
// Explicitly clear any inherited openclaw profile so the runtime doesn't
|
|
300
|
+
// auto-register the gateway or attempt WS reconnects (matches Quick path).
|
|
301
|
+
agentsPatch.openclawProfile = null;
|
|
302
|
+
p.log.info("Standalone mode (no gateway).");
|
|
279
303
|
}
|
|
280
304
|
|
|
281
305
|
const privacy = await stepPrivacy(base, "[4/6] Privacy mode");
|
package/package.json
CHANGED
|
@@ -31,6 +31,12 @@ export interface AgentLoopDeps {
|
|
|
31
31
|
profiler?: Profiler;
|
|
32
32
|
/** Called after each successful SITUATION.md write with the content string. */
|
|
33
33
|
onSituationUpdate?: (content: string) => void;
|
|
34
|
+
/** Predicate to skip SITUATION.md writes entirely when no consumer is
|
|
35
|
+
* listening. Today only the openclaw module reads SITUATION.md, so when
|
|
36
|
+
* no gateway-typed agent is selected this returns false and the disk
|
|
37
|
+
* write is skipped on every tick. Defaults to "always write" if absent
|
|
38
|
+
* (preserves prior behavior for callers that don't pass the predicate). */
|
|
39
|
+
shouldWriteSituation?: () => boolean;
|
|
34
40
|
/** Optional: path to sinain-knowledge.md for startup recap. */
|
|
35
41
|
getKnowledgeDocPath?: () => string | null;
|
|
36
42
|
/** Optional: feedback store for startup recap context. */
|
|
@@ -376,9 +382,14 @@ export class AgentLoop extends EventEmitter {
|
|
|
376
382
|
// Calculate escalation score for both SITUATION.md and escalation check
|
|
377
383
|
const escalationScore = calculateEscalationScore(digest, contextWindow);
|
|
378
384
|
|
|
379
|
-
// Write SITUATION.md
|
|
380
|
-
|
|
381
|
-
|
|
385
|
+
// Write SITUATION.md only when something consumes it (today: an openclaw
|
|
386
|
+
// gateway lane is selected). Without a consumer, skip the disk write to
|
|
387
|
+
// avoid pinning ~/.openclaw/workspace/SITUATION.md on every tick of users
|
|
388
|
+
// who chose claude/openclaude/etc as both lanes.
|
|
389
|
+
if (this.deps.shouldWriteSituation?.() ?? true) {
|
|
390
|
+
const situationContent = writeSituationMd(this.deps.situationMdPath, contextWindow, digest, entry, escalationScore, recorderStatus);
|
|
391
|
+
this.deps.onSituationUpdate?.(situationContent);
|
|
392
|
+
}
|
|
382
393
|
|
|
383
394
|
// Notify for escalation check
|
|
384
395
|
traceCtx?.startSpan("escalation-check");
|
|
@@ -170,17 +170,38 @@ export class Escalator {
|
|
|
170
170
|
log(TAG, `user command set: "${preview}"`);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/** True iff a gateway-typed profile is the active agent on at least one
|
|
174
|
+
* lane. WS-bearing operations (connect, reset, situation push) are gated
|
|
175
|
+
* on this so a user with a configured-but-unselected gateway pays no
|
|
176
|
+
* reconnect tax. */
|
|
177
|
+
private isGatewayLaneSelected(): boolean {
|
|
178
|
+
const isGw = this.deps.isGatewayAgent;
|
|
179
|
+
if (!isGw) return false;
|
|
180
|
+
const esc = this.deps.getEscalationAgent?.() ?? "";
|
|
181
|
+
const spawn = this.deps.getSpawnAgent?.() ?? "";
|
|
182
|
+
return isGw(esc) || isGw(spawn);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Public predicate so the agent loop / index.ts can ask "should I do
|
|
186
|
+
* openclaw-only side-effects on this tick?" without depending on the
|
|
187
|
+
* internals. Mirrors isGatewayLaneSelected. */
|
|
188
|
+
shouldDriveGateway(): boolean {
|
|
189
|
+
const wsConfigured = !!this.deps.openclawConfig.gatewayWsUrl;
|
|
190
|
+
return wsConfigured && this.isGatewayLaneSelected();
|
|
191
|
+
}
|
|
192
|
+
|
|
173
193
|
/** Start the WS connection to OpenClaw.
|
|
174
194
|
*
|
|
175
|
-
* Connects whenever the gateway URL is configured AND
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
195
|
+
* Connects whenever the gateway URL is configured AND a gateway-typed
|
|
196
|
+
* profile is selected on a lane AND escalation isn't fully off. WS is
|
|
197
|
+
* the transport for the openclaw lane — the user selects it via the
|
|
198
|
+
* overlay's agent picker, and dispatch routes accordingly. Removing the
|
|
199
|
+
* openclaw profile from agents.json (and unsetting the env vars) leaves
|
|
200
|
+
* gatewayWsUrl empty → no connect attempt. Likewise, if the profile
|
|
201
|
+
* exists but no lane selects it, no connect attempt.
|
|
180
202
|
*/
|
|
181
203
|
start(): void {
|
|
182
|
-
|
|
183
|
-
if (this.deps.escalationConfig.mode !== "off" && wsConfigured) {
|
|
204
|
+
if (this.deps.escalationConfig.mode !== "off" && this.shouldDriveGateway()) {
|
|
184
205
|
this.wsClient.connect();
|
|
185
206
|
const tokenHash = this.deps.openclawConfig.gatewayToken
|
|
186
207
|
? createHash("sha256").update(this.deps.openclawConfig.gatewayToken).digest("hex").slice(0, 12)
|
|
@@ -194,11 +215,26 @@ export class Escalator {
|
|
|
194
215
|
this.wsClient.disconnect();
|
|
195
216
|
}
|
|
196
217
|
|
|
218
|
+
/** Re-evaluate WS lifecycle after lane selection changes. Connects when a
|
|
219
|
+
* gateway lane just got selected; disconnects when the user moved off
|
|
220
|
+
* every gateway lane. Called from the set_agent overlay handler. */
|
|
221
|
+
evaluateGatewayLifecycle(): void {
|
|
222
|
+
const shouldConnect =
|
|
223
|
+
this.deps.escalationConfig.mode !== "off" && this.shouldDriveGateway();
|
|
224
|
+
if (shouldConnect && !this.wsClient.isConnected) {
|
|
225
|
+
log(TAG, "lane switched to gateway — connecting WS");
|
|
226
|
+
this.wsClient.resetConnection();
|
|
227
|
+
} else if (!shouldConnect && this.wsClient.isConnected) {
|
|
228
|
+
log(TAG, "lane switched off gateway — disconnecting WS");
|
|
229
|
+
this.wsClient.disconnect();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
197
233
|
/** Update escalation mode at runtime. */
|
|
198
234
|
setMode(mode: EscalatorDeps["escalationConfig"]["mode"]): void {
|
|
199
235
|
const wasOff = this.deps.escalationConfig.mode === "off";
|
|
200
236
|
this.deps.escalationConfig.mode = mode;
|
|
201
|
-
if (mode !== "off" && !this.wsClient.isConnected) {
|
|
237
|
+
if (mode !== "off" && !this.wsClient.isConnected && this.shouldDriveGateway()) {
|
|
202
238
|
this.wsClient.resetConnection();
|
|
203
239
|
}
|
|
204
240
|
if (mode === "off") {
|
package/sinain-core/src/index.ts
CHANGED
|
@@ -743,6 +743,11 @@ async function main() {
|
|
|
743
743
|
onSituationUpdate: (content) => {
|
|
744
744
|
escalator.pushSituationMd(content);
|
|
745
745
|
},
|
|
746
|
+
// Gate SITUATION.md writes (and the subsequent push) on a gateway lane
|
|
747
|
+
// being active — see escalator.shouldDriveGateway. Users with no openclaw
|
|
748
|
+
// profile, or with the profile but no lane selecting it, pay zero disk
|
|
749
|
+
// I/O on every tick.
|
|
750
|
+
shouldWriteSituation: () => escalator.shouldDriveGateway(),
|
|
746
751
|
onHudUpdate: (text) => {
|
|
747
752
|
wsHandler.broadcastRaw({ type: "thinking", active: false } as any);
|
|
748
753
|
wsHandler.broadcast(text, "normal", "stream");
|
|
@@ -1241,6 +1246,12 @@ async function main() {
|
|
|
1241
1246
|
// Spawn "off" just means run.sh won't poll /spawn/pending; no
|
|
1242
1247
|
// server-side state to flip. Queued spawn tasks TTL out naturally.
|
|
1243
1248
|
}
|
|
1249
|
+
// Re-evaluate WS lifecycle: connect when a gateway lane just got
|
|
1250
|
+
// selected (zero attempts before this point), disconnect when the user
|
|
1251
|
+
// moved off every gateway lane. This is what makes the "no resources
|
|
1252
|
+
// when not in use" guarantee hold across runtime selection changes,
|
|
1253
|
+
// not just startup config.
|
|
1254
|
+
escalator.evaluateGatewayLifecycle();
|
|
1244
1255
|
// Rebroadcast state so the overlay sees the switch immediately, and
|
|
1245
1256
|
// the bare agent sees it on its next poll-response config piggyback.
|
|
1246
1257
|
// `escalation` field reflects the current escalator mode so the flash
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { AgentEntry, ContextWindow } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Domain-generic events emitted by sinain-core. Optional agent modules
|
|
6
|
+
* (currently just openclaw, future bridges to other gateways) subscribe
|
|
7
|
+
* to these instead of being wired directly into Escalator. The contract
|
|
8
|
+
* here is the only stable surface modules can rely on.
|
|
9
|
+
*/
|
|
10
|
+
export interface CoreEvents {
|
|
11
|
+
/** An escalation has been routed to an agent. Modules whose roster
|
|
12
|
+
* matches `agent` should run their delivery (WS, HTTP, etc). */
|
|
13
|
+
"escalation:dispatched": {
|
|
14
|
+
entry: AgentEntry;
|
|
15
|
+
contextWindow: ContextWindow;
|
|
16
|
+
message: string;
|
|
17
|
+
agent: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** User typed a direct message in the overlay command input. */
|
|
21
|
+
"user:direct": { text: string; ts: number };
|
|
22
|
+
|
|
23
|
+
/** Periodic feedback summary ready for forwarding to long-running agents. */
|
|
24
|
+
"feedback:periodic": { summary: string; lastNTicks: number };
|
|
25
|
+
|
|
26
|
+
/** Agent loop completed an analysis tick. Used by SITUATION.md writers
|
|
27
|
+
* and other context consumers. */
|
|
28
|
+
"tick:complete": { entry: AgentEntry };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CoreEventBus {
|
|
32
|
+
on<K extends keyof CoreEvents>(
|
|
33
|
+
event: K,
|
|
34
|
+
listener: (payload: CoreEvents[K]) => void,
|
|
35
|
+
): () => void;
|
|
36
|
+
emit<K extends keyof CoreEvents>(event: K, payload: CoreEvents[K]): void;
|
|
37
|
+
removeAllListeners(): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class TypedEventBus implements CoreEventBus {
|
|
41
|
+
private readonly emitter = new EventEmitter();
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
// Default Node limit (10) is too low for a long-running bus with many
|
|
45
|
+
// subscribers across the lifetime of the process. Disable the warning;
|
|
46
|
+
// we control the listener set internally.
|
|
47
|
+
this.emitter.setMaxListeners(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
on<K extends keyof CoreEvents>(
|
|
51
|
+
event: K,
|
|
52
|
+
listener: (payload: CoreEvents[K]) => void,
|
|
53
|
+
): () => void {
|
|
54
|
+
this.emitter.on(event, listener as (...args: unknown[]) => void);
|
|
55
|
+
return () => this.emitter.off(event, listener as (...args: unknown[]) => void);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
emit<K extends keyof CoreEvents>(event: K, payload: CoreEvents[K]): void {
|
|
59
|
+
this.emitter.emit(event, payload);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
removeAllListeners(): void {
|
|
63
|
+
this.emitter.removeAllListeners();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createCoreEventBus(): CoreEventBus {
|
|
68
|
+
return new TypedEventBus();
|
|
69
|
+
}
|