Skip to content

Commit 3a500bc

Browse files
authored
Merge branch 'main' into fix/autoindex-skip-limit-value-1466
2 parents dc24d58 + 010569f commit 3a500bc

12 files changed

Lines changed: 637 additions & 32 deletions

‎scripts/extract_nomic_vectors.py‎

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,9 +332,18 @@ def write_vectors_h(path: str, token_count: int, dim: int, incbin_path: str):
332332

333333

334334
def write_blob_s(path: str, incbin_path: str):
335-
"""Write assembler .incbin directive."""
336-
with open(path, "w") as f:
337-
f.write(f"""/* nomic-embed-code vector blob embedded via assembler. */
335+
"""Write the assembler .incbin wrapper for every target object format.
336+
337+
Must stay byte-identical to the tracked vendored/nomic/code_vectors_blob.S:
338+
tests/test_nomic_blob_generator_contract.sh fails if the two drift. The ELF
339+
branch's .note.GNU-stack is load-bearing -- regenerating without it puts an
340+
executable stack back into every Linux release binary.
341+
"""
342+
with open(path, "w", encoding="utf-8") as f:
343+
f.write(f"""/* nomic-embed-code vector blob embedded via assembler.
344+
* Cross-platform: macOS (Mach-O) vs Linux (ELF) vs Windows (COFF). */
345+
346+
#if defined(__APPLE__)
338347
.section __DATA,__const
339348
.globl _PRETRAINED_VECTOR_BLOB
340349
.globl _PRETRAINED_VECTOR_BLOB_LEN
@@ -347,6 +356,46 @@ def write_blob_s(path: str, incbin_path: str):
347356
.p2align 2
348357
_PRETRAINED_VECTOR_BLOB_LEN:
349358
.long _PRETRAINED_VECTOR_BLOB_END - _PRETRAINED_VECTOR_BLOB
359+
360+
#elif defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__)
361+
.section .rdata,"dr"
362+
.globl PRETRAINED_VECTOR_BLOB
363+
.globl PRETRAINED_VECTOR_BLOB_LEN
364+
.p2align 4
365+
PRETRAINED_VECTOR_BLOB:
366+
.incbin "{incbin_path}"
367+
PRETRAINED_VECTOR_BLOB_END:
368+
369+
.section .rdata,"dr"
370+
.p2align 2
371+
PRETRAINED_VECTOR_BLOB_LEN:
372+
.long PRETRAINED_VECTOR_BLOB_END - PRETRAINED_VECTOR_BLOB
373+
374+
#else
375+
/* WHY: an ELF object that carries no .note.GNU-stack tells the linker
376+
* nothing about its stack requirement, and GNU ld then assumes the WORST
377+
* for the whole link — every Linux release binary shipped GNU_STACK RWE
378+
* because of this one omission. This is the only assembly source in the
379+
* build, so it alone decided that property. The note must stay even though
380+
* a blob of constant data obviously never executes: absence is the signal,
381+
* not the contents. -Wl,-z,noexecstack in the link flags enforces the
382+
* outcome, and scripts/ci/check-binary-composition.sh fails the release if
383+
* an executable stack ever comes back. */
384+
.section .note.GNU-stack,"",@progbits
385+
386+
.section .rodata,"a",@progbits
387+
.globl PRETRAINED_VECTOR_BLOB
388+
.globl PRETRAINED_VECTOR_BLOB_LEN
389+
.p2align 4
390+
PRETRAINED_VECTOR_BLOB:
391+
.incbin "{incbin_path}"
392+
PRETRAINED_VECTOR_BLOB_END:
393+
394+
.section .rodata,"a",@progbits
395+
.p2align 2
396+
PRETRAINED_VECTOR_BLOB_LEN:
397+
.long PRETRAINED_VECTOR_BLOB_END - PRETRAINED_VECTOR_BLOB
398+
#endif
350399
""")
351400
print(f" {path}: written")
352401

‎scripts/test.sh‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ bash "$ROOT/tests/test_runtime_isolation_contract.sh"
263263
echo "=== Step 0u: shell line-ending contract ==="
264264
bash "$ROOT/tests/test_shell_line_endings.sh"
265265

266+
echo "=== Step 0v: nomic blob generator contract ==="
267+
bash "$ROOT/tests/test_nomic_blob_generator_contract.sh"
268+
266269
# Verify compiler supports target arch
267270
verify_compiler "$CC"
268271

‎src/cli/client_adapter.c‎

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,39 @@ static void sb_append(adapter_sb_t *sb, const char *s) {
4949
sb->buf[sb->len] = '\0';
5050
}
5151

52+
/* Append a single-quoted JavaScript string literal, escaping the characters
53+
* that could terminate or corrupt it. Avoids the fixed buffer sizing that
54+
* cbm_client_adapter_escape_js imposes, so long registry descriptions fit. */
55+
static void sb_append_js_string(adapter_sb_t *sb, const char *s) {
56+
if (!s) {
57+
sb_append(sb, "''");
58+
return;
59+
}
60+
sb_append(sb, "'");
61+
for (const char *p = s; *p; p++) {
62+
switch (*p) {
63+
case '\\':
64+
sb_append(sb, "\\\\");
65+
break;
66+
case '\'':
67+
sb_append(sb, "\\'");
68+
break;
69+
case '\n':
70+
sb_append(sb, "\\n");
71+
break;
72+
case '\r':
73+
sb_append(sb, "\\r");
74+
break;
75+
default: {
76+
char ch[2] = {*p, '\0'};
77+
sb_append(sb, ch);
78+
break;
79+
}
80+
}
81+
}
82+
sb_append(sb, "'");
83+
}
84+
5285
bool cbm_client_adapter_escape_js(const char *in, char *out, size_t out_sz) {
5386
if (!in || !out || out_sz == 0) {
5487
return false;
@@ -120,6 +153,10 @@ char *cbm_client_adapter_pi(const char *binary_path) {
120153

121154
adapter_sb_t sb = {0};
122155
emit_header(&sb, "pi");
156+
/* Pin the coding-agent contract so a probe against @mariozechner/pi
157+
* (pods CLI) or an old AgentTool arity cannot be mistaken for this file. */
158+
sb_append(&sb, "// Target: @earendil-works/pi-coding-agent >= 0.74.0 (verified 0.84.2)\n"
159+
"// ToolDefinition.execute(toolCallId, params, signal, onUpdate, ctx)\n");
123160
sb_append(&sb, "import { spawn } from 'node:child_process';\n\n");
124161
sb_append(&sb, "const BIN = '");
125162
sb_append(&sb, bin);
@@ -131,7 +168,7 @@ char *cbm_client_adapter_pi(const char *binary_path) {
131168
sb_append(
132169
&sb, "async function call(tool, args, signal) {\n"
133170
" return new Promise((resolve) => {\n"
134-
" const child = spawn(BIN, ['cli', tool, JSON.stringify(args ?? {})], {\n"
171+
" const child = spawn(BIN, ['cli', '--json', tool, JSON.stringify(args ?? {})], {\n"
135172
" stdio: ['ignore', 'pipe', 'pipe'],\n"
136173
" env: { ...process.env, CBM_LOG_LEVEL: 'error' },\n"
137174
" });\n"
@@ -170,11 +207,37 @@ char *cbm_client_adapter_pi(const char *binary_path) {
170207
if (!name || !name[0]) {
171208
continue;
172209
}
173-
sb_append(&sb, " pi.registerTool({ name: '");
174-
sb_append(&sb, name);
175-
sb_append(&sb, "', run: (args, ctx) => call('");
176-
sb_append(&sb, name);
177-
sb_append(&sb, "', args, ctx?.signal) });\n");
210+
const char *title = cbm_mcp_tool_title(name);
211+
const char *description = cbm_mcp_tool_description(name);
212+
const char *schema = cbm_mcp_tool_input_schema(name);
213+
sb_append(&sb, " pi.registerTool({\n");
214+
sb_append(&sb, " name: ");
215+
sb_append_js_string(&sb, name);
216+
sb_append(&sb, ",\n label: ");
217+
sb_append_js_string(&sb, title ? title : name);
218+
sb_append(&sb, ",\n description: ");
219+
sb_append_js_string(&sb, description ? description : "");
220+
sb_append(&sb, ",\n parameters: ");
221+
/* input_schema is compact JSON, which is a valid JavaScript object
222+
* literal; embedding it directly keeps the generated module free of a
223+
* JSON.parse indirection and of any escaping drift. */
224+
sb_append(&sb, schema ? schema : "{}");
225+
/* 0.84.2 calls execute(toolCallId, params, signal, onUpdate, ctx).
226+
* The previous (args, ctx) shape bound the call id as the MCP args. */
227+
sb_append(&sb, ",\n execute: async (toolCallId, params, signal, _onUpdate, ctx) => {\n");
228+
sb_append(&sb, " const result = await call(");
229+
sb_append_js_string(&sb, name);
230+
sb_append(&sb,
231+
", params, signal ?? ctx?.signal);\n"
232+
" if (result && typeof result === 'object' && result.error) {\n"
233+
" throw new Error(String(result.error));\n"
234+
" }\n"
235+
" const content = result && Array.isArray(result.content)\n"
236+
" ? result.content\n"
237+
" : [{ type: 'text', text: JSON.stringify(result ?? null, null, 2) }];\n"
238+
" return { content, details: result ?? {} };\n"
239+
" },\n"
240+
" });\n");
178241
}
179242
sb_append(&sb, "}\n");
180243

‎src/main.c‎

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ enum {
4444
MAIN_PATH_CAP = 4096,
4545
MAIN_CONNECT_TIMEOUT_MS = 1000,
4646
MAIN_STARTUP_TIMEOUT_MS = 10000,
47+
/* Backstop for waiting out a held startup transition — see
48+
* main_local_transition_acquire. Not a budget for healthy contention: a busy
49+
* lock always resolves, either because the live holder finishes or because
50+
* the OS finishes reclaiming a dead holder's lock. This only bounds a peer
51+
* that never finishes, so a command cannot hang indefinitely. */
52+
MAIN_STARTUP_CONTENTION_CEILING_MS = 120000,
4753
MAIN_MCP_STARTUP_TIMEOUT_MS = 30000,
4854
MAIN_REQUEST_TIMEOUT_MS = 24 * 60 * 60 * 1000,
4955
MAIN_HOOK_CONNECT_TIMEOUT_MS = 250,
@@ -1288,14 +1294,47 @@ static bool main_local_cli_feedback_enabled(int argc, char **argv) {
12881294
return cbm_cli_progress_enabled(requested, cli_isatty(2) != 0);
12891295
}
12901296

1297+
/* Acquire the exclusive startup transition, waiting out whatever currently holds it.
1298+
*
1299+
* try_acquire answers one of three things, and they must not be conflated:
1300+
* 1 acquired
1301+
* -1 coordination is unsafe or unverifiable — fail now, waiting cannot help
1302+
* 0 BUSY: the lock is held
1303+
*
1304+
* A busy lock always resolves, by one of two routes:
1305+
* - a live peer holds it, and releases when its command finishes;
1306+
* - the holder is already dead and the operating system has not finished
1307+
* reclaiming the lock yet.
1308+
*
1309+
* The second route is Windows-specific and is why this wait needs room. On POSIX
1310+
* the kernel drops flock the instant the owner dies. Windows byte-range locks do
1311+
* not work that way: Microsoft documents that after a process terminates holding
1312+
* one, "the time it takes for the operating system to unlock these locks depends
1313+
* upon available system resources", and that until then "access to these files
1314+
* may be denied". A loaded CI runner is precisely where those resources are
1315+
* scarce, and `tests/windows/test_daemon_stability.py` manufactures the situation
1316+
* deliberately — it hard-kills daemons with `taskkill /F`, including a crash
1317+
* recovery section, so the next client meets a lock whose owner no longer exists.
1318+
*
1319+
* Waiting was previously capped at MAIN_STARTUP_TIMEOUT_MS (10s), which let a
1320+
* clock decide a user-visible outcome: a command was refused with "coordination
1321+
* remained busy" while the only thing wrong was that the OS had not yet swept up
1322+
* after a killed process. Since both routes above terminate, the correct response
1323+
* to busy is to keep waiting; the ceiling below is a backstop against a peer that
1324+
* never finishes, not a budget for healthy contention. Clean exits already
1325+
* release through main_local_transition_close, so this path is only reached after
1326+
* an abrupt termination or under genuine concurrency. */
12911327
static int main_local_transition_acquire(const cbm_daemon_ipc_endpoint_t *endpoint, FILE *feedback,
12921328
cbm_daemon_ipc_local_transition_t **transition_out) {
1293-
uint64_t deadline = main_deadline_after(MAIN_STARTUP_TIMEOUT_MS);
1329+
uint64_t ceiling = main_deadline_after(MAIN_STARTUP_CONTENTION_CEILING_MS);
12941330
bool waiting_reported = false;
12951331
for (;;) {
12961332
int status = cbm_daemon_ipc_local_transition_try_acquire(endpoint, transition_out);
1297-
if (status != 0 || cbm_now_ms() >= deadline) {
1298-
return status;
1333+
if (status != 0) {
1334+
return status; /* acquired, or a genuine coordination failure */
1335+
}
1336+
if (cbm_now_ms() >= ceiling) {
1337+
return 0; /* still busy after the backstop */
12991338
}
13001339
if (feedback && !waiting_reported) {
13011340
(void)fputs("Waiting for CBM startup coordination...\n", feedback);
@@ -2542,11 +2581,24 @@ int main(int argc, char **argv) {
25422581
int transition_status =
25432582
main_local_transition_acquire(local_endpoint, feedback, &local_transition);
25442583
if (transition_status != 1 || !local_transition) {
2545-
(void)fprintf(stderr,
2546-
"codebase-memory-mcp: CLI startup coordination %s; retry after the "
2547-
"active CBM transition exits\n",
2548-
transition_status == 0 ? "remained busy"
2549-
: "could not be verified safely");
2584+
if (transition_status == 0) {
2585+
/* The backstop fired. Name both explanations: after this much
2586+
* waiting the likely causes are a peer that is genuinely stuck,
2587+
* or (on Windows) a lock left behind by a force-killed process
2588+
* that the OS has not reclaimed. "Busy" alone sent reporters
2589+
* hunting for a CBM session that had already exited. */
2590+
(void)fprintf(stderr,
2591+
"codebase-memory-mcp: CLI startup coordination stayed busy for "
2592+
"%d seconds. Either another CBM command is still running, or a "
2593+
"previous one was force-killed and the operating system has not "
2594+
"released its lock yet. Check for running CBM processes; if there "
2595+
"are none, retry shortly.\n",
2596+
MAIN_STARTUP_CONTENTION_CEILING_MS / 1000);
2597+
} else {
2598+
(void)fprintf(stderr, "codebase-memory-mcp: CLI startup coordination could not "
2599+
"be verified safely; retry after active CBM sessions "
2600+
"exit\n");
2601+
}
25502602
goto local_cli_cleanup;
25512603
}
25522604
int seal_status = cbm_daemon_ipc_local_transition_seal_legacy(local_transition);

‎src/mcp/mcp.c‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,30 @@ const char *cbm_mcp_tool_name(int index) {
946946
return TOOLS[index].name;
947947
}
948948

949+
const char *cbm_mcp_tool_title(const char *tool_name) {
950+
if (!tool_name) {
951+
return NULL;
952+
}
953+
for (int i = 0; i < TOOL_COUNT; i++) {
954+
if (strcmp(TOOLS[i].name, tool_name) == 0) {
955+
return TOOLS[i].title;
956+
}
957+
}
958+
return NULL;
959+
}
960+
961+
const char *cbm_mcp_tool_description(const char *tool_name) {
962+
if (!tool_name) {
963+
return NULL;
964+
}
965+
for (int i = 0; i < TOOL_COUNT; i++) {
966+
if (strcmp(TOOLS[i].name, tool_name) == 0) {
967+
return TOOLS[i].description;
968+
}
969+
}
970+
return NULL;
971+
}
972+
949973
/* Render the top-level --help "Tools:" block from the registry tools/list
950974
* serves. The list used to be hand-maintained in the help text and drifted
951975
* when check_index_coverage was added (#1361); deriving it here makes that

‎src/mcp/mcp.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ const char *cbm_mcp_tool_input_schema(const char *tool_name);
7272
int cbm_mcp_tool_count(void);
7373
const char *cbm_mcp_tool_name(int index);
7474

75+
/* Tool metadata by name (static; do not free; NULL when unknown). Used by the
76+
* generated client adapters so they can emit descriptions and parameter
77+
* schemas without drifting from tools/list. */
78+
const char *cbm_mcp_tool_title(const char *tool_name);
79+
const char *cbm_mcp_tool_description(const char *tool_name);
80+
7581
/* Render the top-level --help "Tools:" block from the registry so the help
7682
* text cannot drift from tools/list (#1361). Heap-allocated; caller frees. */
7783
char *cbm_mcp_tools_help_list(void);

0 commit comments

Comments
 (0)