@possumtech/rummy 2.0.0 → 2.1.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.
- package/.env.example +31 -5
- package/BENCH_ENVIRONMENT.md +230 -0
- package/CLIENT_INTERFACE.md +396 -0
- package/PLUGINS.md +93 -1
- package/SPEC.md +389 -28
- package/bin/postinstall.js +2 -2
- package/bin/rummy.js +2 -2
- package/last_run.txt +5617 -0
- package/migrations/001_initial_schema.sql +2 -1
- package/package.json +13 -9
- package/scriptify/ask_run.js +77 -0
- package/scriptify/cache_probe.js +66 -0
- package/scriptify/cache_probe_grok.js +74 -0
- package/service.js +22 -11
- package/src/agent/AgentLoop.js +62 -157
- package/src/agent/ContextAssembler.js +2 -9
- package/src/agent/Entries.js +54 -98
- package/src/agent/ProjectAgent.js +4 -11
- package/src/agent/TurnExecutor.js +48 -83
- package/src/agent/XmlParser.js +247 -273
- package/src/agent/budget.js +5 -28
- package/src/agent/config.js +38 -0
- package/src/agent/errors.js +7 -13
- package/src/agent/httpStatus.js +1 -19
- package/src/agent/known_queries.sql +1 -1
- package/src/agent/known_store.sql +12 -2
- package/src/agent/materializeContext.js +15 -18
- package/src/agent/pathEncode.js +5 -0
- package/src/agent/rummyHome.js +9 -0
- package/src/agent/runs.sql +37 -0
- package/src/agent/tokens.js +7 -7
- package/src/hooks/HookRegistry.js +1 -16
- package/src/hooks/Hooks.js +8 -33
- package/src/hooks/PluginContext.js +3 -21
- package/src/hooks/RpcRegistry.js +1 -4
- package/src/hooks/RummyContext.js +6 -16
- package/src/hooks/ToolRegistry.js +5 -15
- package/src/llm/LlmProvider.js +41 -33
- package/src/llm/errors.js +41 -4
- package/src/llm/openaiStream.js +125 -0
- package/src/llm/retry.js +109 -0
- package/src/plugins/budget/budget.js +55 -76
- package/src/plugins/cli/README.md +87 -0
- package/src/plugins/cli/bin.js +61 -0
- package/src/plugins/cli/cli.js +120 -0
- package/src/plugins/env/README.md +2 -1
- package/src/plugins/env/env.js +4 -6
- package/src/plugins/env/envDoc.md +2 -2
- package/src/plugins/error/error.js +23 -23
- package/src/plugins/file/file.js +2 -22
- package/src/plugins/get/get.js +12 -34
- package/src/plugins/get/getDoc.md +8 -6
- package/src/plugins/hedberg/edits.js +1 -11
- package/src/plugins/hedberg/hedberg.js +3 -26
- package/src/plugins/hedberg/normalize.js +1 -5
- package/src/plugins/hedberg/patterns.js +4 -15
- package/src/plugins/hedberg/sed.js +1 -7
- package/src/plugins/helpers.js +28 -20
- package/src/plugins/index.js +25 -41
- package/src/plugins/instructions/README.md +18 -0
- package/src/plugins/instructions/instructions.js +97 -38
- package/src/plugins/instructions/instructions.md +24 -15
- package/src/plugins/instructions/instructions_104.md +5 -4
- package/src/plugins/instructions/instructions_105.md +29 -36
- package/src/plugins/instructions/instructions_106.md +22 -0
- package/src/plugins/instructions/instructions_107.md +17 -0
- package/src/plugins/instructions/instructions_108.md +0 -8
- package/src/plugins/known/README.md +26 -6
- package/src/plugins/known/known.js +37 -34
- package/src/plugins/log/README.md +2 -2
- package/src/plugins/log/log.js +27 -34
- package/src/plugins/ollama/ollama.js +50 -66
- package/src/plugins/openai/openai.js +26 -44
- package/src/plugins/openrouter/openrouter.js +28 -52
- package/src/plugins/policy/README.md +8 -2
- package/src/plugins/policy/policy.js +8 -21
- package/src/plugins/prompt/README.md +22 -0
- package/src/plugins/prompt/prompt.js +14 -16
- package/src/plugins/rm/rm.js +5 -2
- package/src/plugins/rm/rmDoc.md +4 -4
- package/src/plugins/rpc/README.md +2 -1
- package/src/plugins/rpc/rpc.js +62 -48
- package/src/plugins/set/README.md +5 -1
- package/src/plugins/set/set.js +23 -33
- package/src/plugins/set/setDoc.md +1 -1
- package/src/plugins/sh/README.md +2 -1
- package/src/plugins/sh/sh.js +5 -11
- package/src/plugins/sh/shDoc.md +2 -2
- package/src/plugins/stream/README.md +6 -5
- package/src/plugins/stream/stream.js +6 -35
- package/src/plugins/telemetry/telemetry.js +26 -19
- package/src/plugins/think/think.js +4 -7
- package/src/plugins/unknown/unknown.js +8 -13
- package/src/plugins/update/update.js +42 -25
- package/src/plugins/update/updateDoc.md +3 -3
- package/src/plugins/xai/xai.js +30 -20
- package/src/plugins/yolo/yolo.js +159 -0
- package/src/server/ClientConnection.js +17 -47
- package/src/server/SocketServer.js +14 -14
- package/src/server/protocol.js +1 -10
- package/src/sql/functions/slugify.js +5 -7
- package/src/sql/v_model_context.sql +4 -11
- package/turns/cli_1777462658211/turn_001.txt +772 -0
- package/turns/cli_1777462658211/turn_002.txt +606 -0
- package/turns/cli_1777462658211/turn_003.txt +667 -0
- package/turns/cli_1777462658211/turn_004.txt +297 -0
- package/turns/cli_1777462658211/turn_005.txt +301 -0
- package/turns/cli_1777462658211/turn_006.txt +262 -0
- package/turns/cli_1777465095132/turn_001.txt +715 -0
- package/turns/cli_1777465095132/turn_002.txt +236 -0
- package/turns/cli_1777465095132/turn_003.txt +287 -0
- package/turns/cli_1777465095132/turn_004.txt +694 -0
- package/turns/cli_1777465095132/turn_005.txt +422 -0
- package/turns/cli_1777465095132/turn_006.txt +365 -0
- package/turns/cli_1777465095132/turn_007.txt +885 -0
- package/turns/cli_1777465095132/turn_008.txt +1277 -0
- package/turns/cli_1777465095132/turn_009.txt +736 -0
package/src/plugins/xai/xai.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
+
import config from "../../agent/config.js";
|
|
1
2
|
import msg from "../../agent/messages.js";
|
|
3
|
+
import { parseRetryAfter } from "../../llm/errors.js";
|
|
2
4
|
|
|
3
|
-
const FETCH_TIMEOUT =
|
|
4
|
-
if (!FETCH_TIMEOUT) throw new Error("RUMMY_FETCH_TIMEOUT must be set");
|
|
5
|
+
const { FETCH_TIMEOUT } = config;
|
|
5
6
|
|
|
6
7
|
const PROVIDER = "xai";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
* xAI (Grok) LLM provider plugin. Registers with hooks.llm.providers if
|
|
10
|
-
* XAI_BASE_URL is set; inert otherwise. Handles model aliases of the
|
|
11
|
-
* form `xai/{modelName}`. Normalizes xAI's distinct response shape
|
|
12
|
-
* into the common OpenAI-shaped envelope.
|
|
13
|
-
*/
|
|
9
|
+
// Inert unless XAI_BASE_URL set; xai/{model} aliases; normalizes to OpenAI envelope.
|
|
14
10
|
export default class Xai {
|
|
15
11
|
#baseUrl;
|
|
16
12
|
#apiKey;
|
|
@@ -39,6 +35,11 @@ export default class Xai {
|
|
|
39
35
|
const body = { model, input: messages };
|
|
40
36
|
if (options.temperature !== undefined)
|
|
41
37
|
body.temperature = options.temperature;
|
|
38
|
+
// xAI auto-caches per-server; stable prompt_cache_key keeps a multi-
|
|
39
|
+
// turn run pinned to the same backend so the cached prefix actually
|
|
40
|
+
// hits. Without this, requests load-balance and cache_tokens stays
|
|
41
|
+
// near-zero. See https://docs.x.ai/developers/advanced-api-usage/prompt-caching.
|
|
42
|
+
if (options.runAlias) body.prompt_cache_key = options.runAlias;
|
|
42
43
|
|
|
43
44
|
const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT);
|
|
44
45
|
const signal = options.signal
|
|
@@ -56,15 +57,27 @@ export default class Xai {
|
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
if (!response.ok) {
|
|
59
|
-
const
|
|
60
|
+
const errorBody = await response.text();
|
|
61
|
+
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
60
62
|
if (response.status === 401 || response.status === 403) {
|
|
61
|
-
|
|
62
|
-
msg("error.xai_auth", {
|
|
63
|
+
const err = new Error(
|
|
64
|
+
msg("error.xai_auth", {
|
|
65
|
+
status: `${response.status} - ${errorBody}`,
|
|
66
|
+
}),
|
|
63
67
|
);
|
|
68
|
+
err.status = response.status;
|
|
69
|
+
err.body = errorBody;
|
|
70
|
+
throw err;
|
|
64
71
|
}
|
|
65
|
-
|
|
66
|
-
msg("error.xai_api", {
|
|
72
|
+
const err = new Error(
|
|
73
|
+
msg("error.xai_api", {
|
|
74
|
+
status: `${response.status} - ${errorBody}`,
|
|
75
|
+
}),
|
|
67
76
|
);
|
|
77
|
+
err.status = response.status;
|
|
78
|
+
err.body = errorBody;
|
|
79
|
+
err.retryAfter = retryAfter;
|
|
80
|
+
throw err;
|
|
68
81
|
}
|
|
69
82
|
|
|
70
83
|
return this.#normalize(await response.json());
|
|
@@ -133,12 +146,11 @@ export default class Xai {
|
|
|
133
146
|
const modelsUrl = this.#baseUrl.replace(/\/responses$/, "/models");
|
|
134
147
|
const res = await fetch(modelsUrl, {
|
|
135
148
|
headers: { Authorization: `Bearer ${this.#apiKey}` },
|
|
136
|
-
signal: AbortSignal.timeout(
|
|
149
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
137
150
|
});
|
|
138
151
|
if (res.ok) {
|
|
139
152
|
const data = await res.json();
|
|
140
|
-
// xAI
|
|
141
|
-
// depending on the API version; accept either and crash otherwise.
|
|
153
|
+
// xAI /models response shape varies by API version.
|
|
142
154
|
let models;
|
|
143
155
|
if (data.data) models = data.data;
|
|
144
156
|
else if (data.models) models = data.models;
|
|
@@ -156,12 +168,10 @@ export default class Xai {
|
|
|
156
168
|
/\/responses$/,
|
|
157
169
|
`/language-models/${model}`,
|
|
158
170
|
);
|
|
159
|
-
// Optional
|
|
160
|
-
// API versions, timeout, etc.) we fall through to the next strategy
|
|
161
|
-
// below; a terminal throw fires if no strategy resolves.
|
|
171
|
+
// Optional probe; failure falls through to terminal throw below.
|
|
162
172
|
const langRes = await fetch(langUrl, {
|
|
163
173
|
headers: { Authorization: `Bearer ${this.#apiKey}` },
|
|
164
|
-
signal: AbortSignal.timeout(
|
|
174
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
165
175
|
}).catch(() => null);
|
|
166
176
|
if (langRes?.ok) {
|
|
167
177
|
const langData = await langRes.json();
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { logPathToDataBase } from "../helpers.js";
|
|
3
|
+
|
|
4
|
+
const SH_PATH_RE = /^log:\/\/turn_\d+\/(sh|env)\//;
|
|
5
|
+
|
|
6
|
+
// Auto-resolves proposals + spawns sh/env locally for runs started with yolo:true. SPEC #yolo_mode.
|
|
7
|
+
export default class Yolo {
|
|
8
|
+
constructor(core) {
|
|
9
|
+
this.core = core;
|
|
10
|
+
core.hooks.proposal.pending.on(this.#onPending.bind(this));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async #onPending({ proposed, rummy }) {
|
|
14
|
+
if (!rummy?.yolo) return;
|
|
15
|
+
for (const p of proposed) {
|
|
16
|
+
// Resolve first so sh/env's post-accept seeds channels before we stream into them.
|
|
17
|
+
await this.#serverResolve(rummy, p.path);
|
|
18
|
+
if (SH_PATH_RE.test(p.path)) {
|
|
19
|
+
await this.#executeShellProposal(rummy, p.path);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Inline mirror of AgentLoop.resolve()'s accept path.
|
|
25
|
+
async #serverResolve(rummy, path) {
|
|
26
|
+
const runId = rummy.runId;
|
|
27
|
+
const entries = rummy.entries;
|
|
28
|
+
const db = rummy.db;
|
|
29
|
+
const runRow = await db.get_run_by_id.get({ id: runId });
|
|
30
|
+
const project = await db.get_project_by_id.get({ id: runRow.project_id });
|
|
31
|
+
const attrs = await entries.getAttributes(runId, path);
|
|
32
|
+
const ctx = {
|
|
33
|
+
runId,
|
|
34
|
+
runRow,
|
|
35
|
+
projectId: runRow.project_id,
|
|
36
|
+
projectRoot: project?.project_root,
|
|
37
|
+
path,
|
|
38
|
+
attrs,
|
|
39
|
+
output: "",
|
|
40
|
+
db,
|
|
41
|
+
entries,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const veto = await this.core.hooks.proposal.accepting.filter(null, ctx);
|
|
45
|
+
if (veto?.allow === false) {
|
|
46
|
+
await entries.set({
|
|
47
|
+
runId,
|
|
48
|
+
path,
|
|
49
|
+
state: "failed",
|
|
50
|
+
outcome: veto.outcome,
|
|
51
|
+
body: veto.body,
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const resolvedBody = await this.core.hooks.proposal.content.filter("", ctx);
|
|
57
|
+
const existing = await entries.getState(runId, path);
|
|
58
|
+
const existingTurn = existing?.turn === undefined ? 0 : existing.turn;
|
|
59
|
+
await entries.set({
|
|
60
|
+
runId,
|
|
61
|
+
turn: existingTurn,
|
|
62
|
+
path,
|
|
63
|
+
state: "resolved",
|
|
64
|
+
body: resolvedBody,
|
|
65
|
+
});
|
|
66
|
+
await this.core.hooks.proposal.accepted.emit({ ...ctx, resolvedBody });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Spawn locally and stream into {dataBase}_{1,2}; mirrors stream/stream-completed RPC.
|
|
70
|
+
async #executeShellProposal(rummy, logPath) {
|
|
71
|
+
const runId = rummy.runId;
|
|
72
|
+
const entries = rummy.entries;
|
|
73
|
+
const db = rummy.db;
|
|
74
|
+
const runRow = await db.get_run_by_id.get({ id: runId });
|
|
75
|
+
const project = await db.get_project_by_id.get({ id: runRow.project_id });
|
|
76
|
+
const projectRoot = project?.project_root;
|
|
77
|
+
if (!projectRoot) return;
|
|
78
|
+
|
|
79
|
+
const attrs = await entries.getAttributes(runId, logPath);
|
|
80
|
+
const command = attrs?.command || attrs?.summary;
|
|
81
|
+
if (!command) return;
|
|
82
|
+
|
|
83
|
+
const dataBase = logPathToDataBase(logPath);
|
|
84
|
+
if (!dataBase) return;
|
|
85
|
+
const stdoutPath = `${dataBase}_1`;
|
|
86
|
+
const stderrPath = `${dataBase}_2`;
|
|
87
|
+
|
|
88
|
+
const start = Date.now();
|
|
89
|
+
const child = spawn("bash", ["-lc", command], {
|
|
90
|
+
cwd: projectRoot,
|
|
91
|
+
env: process.env,
|
|
92
|
+
});
|
|
93
|
+
// Buffer + write-once-on-exit; async appends would race the terminal-state transition.
|
|
94
|
+
const stdoutChunks = [];
|
|
95
|
+
const stderrChunks = [];
|
|
96
|
+
child.stdout.on("data", (data) => stdoutChunks.push(data.toString()));
|
|
97
|
+
child.stderr.on("data", (data) => stderrChunks.push(data.toString()));
|
|
98
|
+
|
|
99
|
+
await new Promise((resolve) => {
|
|
100
|
+
child.on("close", async (code) => {
|
|
101
|
+
const stdoutBody = stdoutChunks.join("");
|
|
102
|
+
const stderrBody = stderrChunks.join("");
|
|
103
|
+
if (stdoutBody) {
|
|
104
|
+
try {
|
|
105
|
+
await entries.set({
|
|
106
|
+
runId,
|
|
107
|
+
path: stdoutPath,
|
|
108
|
+
body: stdoutBody,
|
|
109
|
+
append: true,
|
|
110
|
+
});
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
if (stderrBody) {
|
|
114
|
+
try {
|
|
115
|
+
await entries.set({
|
|
116
|
+
runId,
|
|
117
|
+
path: stderrPath,
|
|
118
|
+
body: stderrBody,
|
|
119
|
+
append: true,
|
|
120
|
+
});
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
const exitCode = code === null ? 130 : code;
|
|
124
|
+
const duration = `${Math.round((Date.now() - start) / 1000)}s`;
|
|
125
|
+
const terminalState = exitCode === 0 ? "resolved" : "failed";
|
|
126
|
+
const outcome = exitCode === 0 ? null : `exit:${exitCode}`;
|
|
127
|
+
// body=undefined preserves streamed content; body="" would wipe it.
|
|
128
|
+
for (const path of [stdoutPath, stderrPath]) {
|
|
129
|
+
try {
|
|
130
|
+
await entries.set({
|
|
131
|
+
runId,
|
|
132
|
+
path,
|
|
133
|
+
state: terminalState,
|
|
134
|
+
outcome,
|
|
135
|
+
});
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const channels = await entries.getEntriesByPattern(
|
|
140
|
+
runId,
|
|
141
|
+
`${dataBase}_*`,
|
|
142
|
+
null,
|
|
143
|
+
);
|
|
144
|
+
const summary = channels
|
|
145
|
+
.map((c) => `${c.path} (${c.tokens} tokens)`)
|
|
146
|
+
.join(", ");
|
|
147
|
+
const exitLabel = exitCode === 0 ? "exit=0" : `exit=${exitCode}`;
|
|
148
|
+
await entries.set({
|
|
149
|
+
runId,
|
|
150
|
+
path: logPath,
|
|
151
|
+
state: "resolved",
|
|
152
|
+
body: `ran '${command}', ${exitLabel} (${duration}). Output: ${summary}`,
|
|
153
|
+
});
|
|
154
|
+
} catch {}
|
|
155
|
+
resolve();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -23,8 +23,7 @@ export default class ClientConnection {
|
|
|
23
23
|
|
|
24
24
|
this.#ws.on("message", (data) => this.#handleMessage(data));
|
|
25
25
|
this.#ws.on("close", () => {
|
|
26
|
-
// Fire-and-forget
|
|
27
|
-
// server-initiated close can await the same work.
|
|
26
|
+
// Fire-and-forget; shutdown() caches the Promise for server-initiated close to await.
|
|
28
27
|
this.shutdown().catch((err) => {
|
|
29
28
|
console.warn(`[RUMMY] shutdown on ws close failed: ${err.message}`);
|
|
30
29
|
});
|
|
@@ -33,25 +32,6 @@ export default class ClientConnection {
|
|
|
33
32
|
this.#setupNotifications();
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
#onProgress = (payload) => {
|
|
37
|
-
if (payload.projectId === this.#context.projectId) {
|
|
38
|
-
this.#sendNotification("run/progress", {
|
|
39
|
-
run: payload.run,
|
|
40
|
-
turn: payload.turn,
|
|
41
|
-
status: payload.status,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
#onProposal = (payload) => {
|
|
47
|
-
if (payload.projectId === this.#context.projectId) {
|
|
48
|
-
this.#sendNotification("run/proposal", {
|
|
49
|
-
run: payload.run,
|
|
50
|
-
proposed: payload.proposed,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
35
|
#onRender = (payload) => {
|
|
56
36
|
if (payload.projectId === this.#context.projectId) {
|
|
57
37
|
this.#sendNotification("ui/render", {
|
|
@@ -80,44 +60,35 @@ export default class ClientConnection {
|
|
|
80
60
|
}
|
|
81
61
|
};
|
|
82
62
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
63
|
+
// Pulse: any entry write in this client's project. Content-free hint
|
|
64
|
+
// — client reconciles via getEntriesByPattern with `since`.
|
|
65
|
+
#onEntryChanged = async ({ runId, path, changeType }) => {
|
|
66
|
+
if (this.#context.projectId == null) return;
|
|
67
|
+
const run = await this.#db.get_run_by_id.get({ id: runId });
|
|
68
|
+
if (!run || run.project_id !== this.#context.projectId) return;
|
|
69
|
+
this.#sendNotification("run/changed", {
|
|
70
|
+
run: run.alias,
|
|
71
|
+
runId,
|
|
72
|
+
path,
|
|
73
|
+
changeType,
|
|
74
|
+
});
|
|
95
75
|
};
|
|
96
76
|
|
|
97
77
|
#setupNotifications() {
|
|
98
|
-
this.#hooks.run.progress.on(this.#onProgress);
|
|
99
|
-
this.#hooks.proposal.pending.on(this.#onProposal);
|
|
100
78
|
this.#hooks.ui.render.on(this.#onRender);
|
|
101
79
|
this.#hooks.ui.notify.on(this.#onNotify);
|
|
102
|
-
this.#hooks.run.state.on(this.#onState);
|
|
103
80
|
this.#hooks.stream.cancelled.on(this.#onStreamCancelled);
|
|
81
|
+
this.#hooks.entry.changed.on(this.#onEntryChanged);
|
|
104
82
|
}
|
|
105
83
|
|
|
106
84
|
#teardown() {
|
|
107
|
-
this.#hooks.run.progress.off(this.#onProgress);
|
|
108
|
-
this.#hooks.proposal.pending.off(this.#onProposal);
|
|
109
85
|
this.#hooks.ui.render.off(this.#onRender);
|
|
110
86
|
this.#hooks.ui.notify.off(this.#onNotify);
|
|
111
|
-
this.#hooks.run.state.off(this.#onState);
|
|
112
87
|
this.#hooks.stream.cancelled.off(this.#onStreamCancelled);
|
|
88
|
+
this.#hooks.entry.changed.off(this.#onEntryChanged);
|
|
113
89
|
}
|
|
114
90
|
|
|
115
|
-
|
|
116
|
-
* Abort in-flight runs on this connection and wait for them to
|
|
117
|
-
* settle. Idempotent: `ws.on("close")` and server-initiated close
|
|
118
|
-
* both call this; the cached Promise guarantees the work happens
|
|
119
|
-
* exactly once and both callers observe the same completion.
|
|
120
|
-
*/
|
|
91
|
+
// Idempotent abort+drain; cached Promise lets ws.close and server.close share completion.
|
|
121
92
|
shutdown() {
|
|
122
93
|
if (!this.#shutdownPromise) {
|
|
123
94
|
this.#shutdownPromise = (async () => {
|
|
@@ -241,8 +212,7 @@ export default class ClientConnection {
|
|
|
241
212
|
} catch (error) {
|
|
242
213
|
console.error(`[RUMMY] RPC Error: ${error.message}`);
|
|
243
214
|
console.error(`[RUMMY] Stack: ${error.stack}`);
|
|
244
|
-
// JSON-RPC
|
|
245
|
-
// MUST carry null per the spec.
|
|
215
|
+
// JSON-RPC requires null id for malformed requests with no id.
|
|
246
216
|
this.#send({
|
|
247
217
|
jsonrpc: "2.0",
|
|
248
218
|
error: { code: -32603, message: error.message },
|
|
@@ -15,18 +15,13 @@ export default class SocketServer {
|
|
|
15
15
|
this.#wss.on("connection", (ws, _req) => {
|
|
16
16
|
const conn = new ClientConnection(ws, this.#db, this.#hooks);
|
|
17
17
|
this.#connections.add(conn);
|
|
18
|
-
//
|
|
19
|
-
// shutdown drain has fully settled — not on raw ws-close —
|
|
20
|
-
// so server close() can still find and await an in-progress
|
|
21
|
-
// shutdown kicked off by a client-initiated disconnect.
|
|
18
|
+
// Delete after drain settles so server.close() can await client-initiated shutdowns.
|
|
22
19
|
ws.on("close", () => {
|
|
23
20
|
conn.shutdown().finally(() => this.#connections.delete(conn));
|
|
24
21
|
});
|
|
25
22
|
});
|
|
26
23
|
|
|
27
|
-
this.#wss.on("error", (_err) => {
|
|
28
|
-
// Proxy to registry or handle locally
|
|
29
|
-
});
|
|
24
|
+
this.#wss.on("error", (_err) => {});
|
|
30
25
|
}
|
|
31
26
|
|
|
32
27
|
address() {
|
|
@@ -38,14 +33,19 @@ export default class SocketServer {
|
|
|
38
33
|
}
|
|
39
34
|
|
|
40
35
|
async close() {
|
|
41
|
-
// Drain in-flight runs
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
// Drain in-flight runs first; otherwise detached kickoffs pin the event loop.
|
|
37
|
+
// Best-effort: a single connection failing to shut down cleanly should not
|
|
38
|
+
// prevent the others from closing, but the failure must be visible.
|
|
39
|
+
const results = await Promise.allSettled(
|
|
40
|
+
Array.from(this.#connections, (conn) => conn.shutdown()),
|
|
41
|
+
);
|
|
42
|
+
for (const r of results) {
|
|
43
|
+
if (r.status === "rejected") {
|
|
44
|
+
console.error(
|
|
45
|
+
`[RUMMY] Connection shutdown failed: ${r.reason?.message ?? r.reason}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
47
48
|
}
|
|
48
|
-
await Promise.all(shutdowns);
|
|
49
49
|
this.#connections.clear();
|
|
50
50
|
|
|
51
51
|
await new Promise((resolve) => {
|
package/src/server/protocol.js
CHANGED
|
@@ -1,11 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* Server↔client wire-protocol version. Bumped whenever the RPC shape
|
|
3
|
-
* or notification payload shape changes in a way that breaks existing
|
|
4
|
-
* clients. Clients pass their own version in `rummy/hello`; server
|
|
5
|
-
* rejects MAJOR mismatch. Git commit log is the changelog.
|
|
6
|
-
*
|
|
7
|
-
* MAJOR — breaking change (removed/renamed method, shape change)
|
|
8
|
-
* MINOR — additive change (new method, new optional field)
|
|
9
|
-
* PATCH — internal fix visible to the wire shape
|
|
10
|
-
*/
|
|
1
|
+
// Wire protocol version; rummy/hello rejects MAJOR mismatch. Semver.
|
|
11
2
|
export const RUMMY_PROTOCOL_VERSION = "2.0.0";
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
+
import encodeSegment from "../../agent/pathEncode.js";
|
|
2
|
+
|
|
1
3
|
export const deterministic = true;
|
|
2
4
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// "contents of Document 1" → "contents_of_Document_1" (spaces become underscores)
|
|
6
|
-
// Slice on decoded text, then split-encode-join per segment so / survives as
|
|
7
|
-
// a separator while anything URL-unsafe inside a segment gets escaped.
|
|
5
|
+
// commas→/, then encode-per-segment so / survives as separator.
|
|
6
|
+
// encodeSegment handles spaces→_ + URL-encode (single rule, used everywhere).
|
|
8
7
|
export default function slugify(text) {
|
|
9
8
|
if (!text) return "";
|
|
10
9
|
return text
|
|
11
10
|
.slice(0, 80)
|
|
12
11
|
.replace(/,/g, "/")
|
|
13
|
-
.replace(/ /g, "_")
|
|
14
12
|
.split("/")
|
|
15
13
|
.filter(Boolean)
|
|
16
|
-
.map(
|
|
14
|
+
.map(encodeSegment)
|
|
17
15
|
.join("/");
|
|
18
16
|
}
|
|
@@ -16,8 +16,8 @@ visible AS (
|
|
|
16
16
|
, e.attributes
|
|
17
17
|
, COALESCE(s.category, 'logging') AS category
|
|
18
18
|
, CASE
|
|
19
|
-
WHEN rv.visibility = 'archived' THEN NULL
|
|
20
19
|
WHEN s.model_visible = 0 THEN NULL
|
|
20
|
+
WHEN rv.visibility = 'archived' THEN NULL
|
|
21
21
|
ELSE rv.visibility
|
|
22
22
|
END AS effective_visibility
|
|
23
23
|
FROM run_views AS rv
|
|
@@ -38,10 +38,7 @@ projected AS (
|
|
|
38
38
|
, attributes
|
|
39
39
|
-- Category comes from schemes table — plugins declare it via registerScheme().
|
|
40
40
|
, category
|
|
41
|
-
,
|
|
42
|
-
WHEN effective_visibility IN ('visible', 'summarized') THEN body
|
|
43
|
-
ELSE ''
|
|
44
|
-
END AS body
|
|
41
|
+
, body
|
|
45
42
|
FROM visible
|
|
46
43
|
WHERE effective_visibility IS NOT NULL
|
|
47
44
|
)
|
|
@@ -67,13 +64,9 @@ SELECT
|
|
|
67
64
|
WHEN 'prompt' THEN 5
|
|
68
65
|
ELSE 5
|
|
69
66
|
END
|
|
70
|
-
,
|
|
71
|
-
, CASE visibility
|
|
72
|
-
WHEN 'summarized' THEN 0
|
|
73
|
-
ELSE 1
|
|
74
|
-
END
|
|
67
|
+
, scheme
|
|
75
68
|
, turn
|
|
76
69
|
, updated_at
|
|
77
|
-
,
|
|
70
|
+
, path
|
|
78
71
|
) AS ordinal
|
|
79
72
|
FROM projected;
|