brainclaw 1.16.0 → 1.18.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/README.md +22 -8
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-review.js +2 -2
- package/dist/commands/code-map.js +4 -1
- package/dist/commands/codev.js +61 -30
- package/dist/commands/doctor.js +14 -1
- package/dist/commands/harvest.js +241 -25
- package/dist/commands/inbox.js +10 -4
- package/dist/commands/loop.js +2 -2
- package/dist/commands/loops-handlers.js +82 -1
- package/dist/commands/mcp-catalog.js +12 -4
- package/dist/commands/mcp-read-handlers.js +90 -7
- package/dist/commands/mcp-schemas.generated.js +3 -0
- package/dist/commands/mcp-write-coordination.js +159 -40
- package/dist/commands/mcp.js +11 -2
- package/dist/core/agent-capability.js +7 -2
- package/dist/core/agent-files.js +53 -2
- package/dist/core/agent-integrations.js +1 -0
- package/dist/core/agentrun-reconciler.js +171 -7
- package/dist/core/agentruns.js +6 -1
- package/dist/core/code-map/aggregate.js +473 -0
- package/dist/core/code-map/backend.js +36 -10
- package/dist/core/code-map/freshness.js +36 -1
- package/dist/core/code-map/lang/c/imports.scm +12 -0
- package/dist/core/code-map/lang/c/index.js +150 -0
- package/dist/core/code-map/lang/c/tags.scm +68 -0
- package/dist/core/code-map/lang/cpp/imports.scm +14 -0
- package/dist/core/code-map/lang/cpp/index.js +149 -0
- package/dist/core/code-map/lang/cpp/tags.scm +87 -0
- package/dist/core/code-map/lang/csharp/imports.scm +20 -0
- package/dist/core/code-map/lang/csharp/index.js +224 -0
- package/dist/core/code-map/lang/csharp/tags.scm +63 -0
- package/dist/core/code-map/lang/go/imports.scm +13 -0
- package/dist/core/code-map/lang/go/index.js +139 -0
- package/dist/core/code-map/lang/go/tags.scm +36 -0
- package/dist/core/code-map/lang/providers.js +12 -1
- package/dist/core/code-map/lang/ruby/imports.scm +24 -0
- package/dist/core/code-map/lang/ruby/index.js +198 -0
- package/dist/core/code-map/lang/ruby/tags.scm +49 -0
- package/dist/core/code-map/lang/rust/imports.scm +44 -0
- package/dist/core/code-map/lang/rust/index.js +136 -0
- package/dist/core/code-map/lang/rust/tags.scm +47 -0
- package/dist/core/code-map/query.js +229 -80
- package/dist/core/code-map/types.js +18 -0
- package/dist/core/code-map/work-section.js +8 -7
- package/dist/core/codev-responses.js +16 -0
- package/dist/core/dispatcher.js +209 -29
- package/dist/core/execution-adapters.js +29 -3
- package/dist/core/ideation-loop-close.js +124 -0
- package/dist/core/loops/artifact-resolver.js +197 -0
- package/dist/core/loops/attempt-reservation.js +576 -0
- package/dist/core/loops/commit-intent.js +494 -0
- package/dist/core/loops/facade-schema.js +48 -0
- package/dist/core/loops/impl-bind.js +144 -0
- package/dist/core/loops/index.js +1 -1
- package/dist/core/loops/iteration-engine.js +29 -0
- package/dist/core/loops/lock.js +14 -0
- package/dist/core/loops/project-resolution.js +157 -0
- package/dist/core/loops/reconcile-turn.js +369 -0
- package/dist/core/loops/result-reducers.js +88 -0
- package/dist/core/loops/store.js +46 -7
- package/dist/core/loops/types.js +139 -11
- package/dist/core/loops/verbs.js +9 -3
- package/dist/core/loops/verify-command.js +209 -0
- package/dist/core/messaging.js +58 -5
- package/dist/core/review-loop-close.js +106 -34
- package/dist/core/review-loop-turn-dispatch.js +445 -0
- package/dist/core/runtime-signals.js +68 -0
- package/dist/core/schema.js +34 -0
- package/dist/core/worktree.js +240 -22
- package/dist/facts.js +10 -10
- package/dist/facts.json +9 -9
- package/dist/wasm/tree-sitter-c.wasm +0 -0
- package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/wasm/tree-sitter-cpp.wasm +0 -0
- package/dist/wasm/tree-sitter-go.wasm +0 -0
- package/dist/wasm/tree-sitter-ruby.wasm +0 -0
- package/dist/wasm/tree-sitter-rust.wasm +0 -0
- package/docs/cli.md +1 -1
- package/docs/code-map.md +22 -6
- package/docs/concepts/loop-engine.md +28 -2
- package/docs/concepts/observer-protocol.md +22 -0
- package/docs/integrations/codex.md +19 -3
- package/docs/mcp-schema-changelog.md +43 -1
- package/package.json +1 -1
package/dist/commands/inbox.js
CHANGED
|
@@ -21,12 +21,15 @@ export function runInboxList(options) {
|
|
|
21
21
|
}
|
|
22
22
|
const effectiveCwd = cwd ?? process.cwd();
|
|
23
23
|
const agent = options.agent ?? resolveCurrentAgentName(effectiveCwd) ?? 'unknown';
|
|
24
|
-
|
|
24
|
+
// --all widens past the core actionable default (pln#627 Phase A); without
|
|
25
|
+
// includeAll, a bare undefined status now hides acknowledged + archived.
|
|
26
|
+
const includeAll = options.all === true;
|
|
27
|
+
const status = includeAll ? undefined : options.status ?? 'pending';
|
|
25
28
|
const msgType = options.type;
|
|
26
29
|
const threadId = options.thread;
|
|
27
30
|
let messages;
|
|
28
31
|
if (options.localOnly) {
|
|
29
|
-
const result = readInbox({ agent, status, type: msgType, thread_id: threadId, markAsRead: false }, effectiveCwd);
|
|
32
|
+
const result = readInbox({ agent, status, includeAll, type: msgType, thread_id: threadId, markAsRead: false }, effectiveCwd);
|
|
30
33
|
messages = result.messages;
|
|
31
34
|
}
|
|
32
35
|
else {
|
|
@@ -36,7 +39,7 @@ export function runInboxList(options) {
|
|
|
36
39
|
for (const store of chain) {
|
|
37
40
|
try {
|
|
38
41
|
// Fetch all messages from this store (no pagination limit)
|
|
39
|
-
const storeResult = readInbox({ agent, status, type: msgType, thread_id: threadId, markAsRead: false, limit: 1_000_000 }, store.cwd);
|
|
42
|
+
const storeResult = readInbox({ agent, status, includeAll, type: msgType, thread_id: threadId, markAsRead: false, limit: 1_000_000 }, store.cwd);
|
|
40
43
|
for (const msg of storeResult.messages) {
|
|
41
44
|
if (!seenIds.has(msg.id)) {
|
|
42
45
|
seenIds.add(msg.id);
|
|
@@ -48,7 +51,7 @@ export function runInboxList(options) {
|
|
|
48
51
|
}
|
|
49
52
|
// Fallback when no chain found
|
|
50
53
|
if (messages.length === 0 && chain.length === 0) {
|
|
51
|
-
const result = readInbox({ agent, status, type: msgType, thread_id: threadId, markAsRead: false }, effectiveCwd);
|
|
54
|
+
const result = readInbox({ agent, status, includeAll, type: msgType, thread_id: threadId, markAsRead: false }, effectiveCwd);
|
|
52
55
|
messages = result.messages;
|
|
53
56
|
}
|
|
54
57
|
}
|
|
@@ -140,6 +143,9 @@ export function runInboxSend(to, text, options) {
|
|
|
140
143
|
}
|
|
141
144
|
else {
|
|
142
145
|
console.log(`✔ Message sent: [${result.shortLabel}] ${result.type} → ${result.to}`);
|
|
146
|
+
if (result.warning) {
|
|
147
|
+
console.warn(`⚠ ${result.warning}`);
|
|
148
|
+
}
|
|
143
149
|
}
|
|
144
150
|
}
|
|
145
151
|
catch (err) {
|
package/dist/commands/loop.js
CHANGED
|
@@ -122,14 +122,14 @@ function buildRequest(subcommand, loopId, opts) {
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
export function runLoopCommand(subcommand, args, options = {}, cwd) {
|
|
125
|
+
export async function runLoopCommand(subcommand, args, options = {}, cwd) {
|
|
126
126
|
if (!memoryExists(cwd)) {
|
|
127
127
|
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
128
128
|
process.exit(1);
|
|
129
129
|
}
|
|
130
130
|
const loopId = requireLoopId(args, options);
|
|
131
131
|
const request = buildRequest(subcommand, loopId, options);
|
|
132
|
-
const handled = handleBclawLoop({ args: request, cwd });
|
|
132
|
+
const handled = await handleBclawLoop({ args: request, cwd });
|
|
133
133
|
if (handled.response.status !== 'ok') {
|
|
134
134
|
const message = handled.response.error ?? handled.summary;
|
|
135
135
|
fail(`bclaw_loop.${String(request.intent)} rejected the call: ${message}`, 2, options);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { ZodError } from 'zod';
|
|
2
2
|
import { listAgentRuns } from '../core/agentruns.js';
|
|
3
3
|
import { reconcileAgentRun } from '../core/agentrun-reconciler.js';
|
|
4
|
+
import { findReservationByRunId } from '../core/loops/attempt-reservation.js';
|
|
5
|
+
import { runVerify } from '../core/loops/verify-command.js';
|
|
6
|
+
import { runImplBind } from '../core/loops/impl-bind.js';
|
|
4
7
|
import { add_artifact, advance, AwaitingFileApplyApprovalError, closeLoop, complete_turn, computeNextExpected, getLoop, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, listLoopEvents, listLoops, LockLostError, LockTimeoutError, openLoop, pause, provideInput, requestInput, resume, sweepPauseTimeouts, turn, VersionConflictError, withLoopLock, } from '../core/loops/index.js';
|
|
5
8
|
import { BclawLoopRequestSchema, BCLAW_LOOP_INTENTS, } from '../core/loops/facade-schema.js';
|
|
6
9
|
// NextExpectedHint type now lives in src/core/loops/next-expected.ts
|
|
@@ -179,7 +182,7 @@ function trySweepLoopTimeouts(loop_id, cwd) {
|
|
|
179
182
|
}
|
|
180
183
|
catch { /* best-effort: never block facade on sweep errors */ }
|
|
181
184
|
}
|
|
182
|
-
export function handleBclawLoop(options) {
|
|
185
|
+
export async function handleBclawLoop(options) {
|
|
183
186
|
const startMs = Date.now();
|
|
184
187
|
const defaultActor = options.defaultActor ?? 'bclaw_loop';
|
|
185
188
|
const inferredIntent = inferIntent(options.args);
|
|
@@ -218,6 +221,7 @@ export function handleBclawLoop(options) {
|
|
|
218
221
|
linked: req.linked,
|
|
219
222
|
stop_condition: req.stop_condition,
|
|
220
223
|
mode: req.mode,
|
|
224
|
+
verify: req.verify,
|
|
221
225
|
created_by: agentId,
|
|
222
226
|
}, options.cwd);
|
|
223
227
|
const newEvents = findNewLoopEvents(loop.id, undefined, options.cwd);
|
|
@@ -257,7 +261,20 @@ export function handleBclawLoop(options) {
|
|
|
257
261
|
continue;
|
|
258
262
|
if (slotStatus === 'done' || slotStatus === 'failed' || slotStatus === 'cancelled')
|
|
259
263
|
continue;
|
|
264
|
+
// pln#630 PR2b-c (§13 R6): GET is strictly observational for
|
|
265
|
+
// TURN-OWNED slots. A slot carrying current_turn_id reconciles only
|
|
266
|
+
// via the dedicated mutating reconcile path (never on a read), so a
|
|
267
|
+
// stale/racing read can't phantom-complete its run. Legacy slots
|
|
268
|
+
// (no current_turn_id) keep the intentional lazy reconcile
|
|
269
|
+
// (trp_fdf3e590) that converges silent-completion on access.
|
|
270
|
+
if (slot.current_turn_id)
|
|
271
|
+
continue;
|
|
260
272
|
for (const run of listAgentRuns(options.cwd, { assignment_id: assignmentId })) {
|
|
273
|
+
// Belt-and-braces (review PR2b-c #D): skip by ACTUAL ownership too,
|
|
274
|
+
// not just the slot pointer — if a run is turn-owned but its slot
|
|
275
|
+
// wasn't stamped (write-ordering), GET must still not mutate it.
|
|
276
|
+
if (findReservationByRunId(run.id, options.cwd))
|
|
277
|
+
continue;
|
|
261
278
|
reconcileAgentRun(run.id, options.cwd);
|
|
262
279
|
}
|
|
263
280
|
}
|
|
@@ -426,6 +443,70 @@ export function handleBclawLoop(options) {
|
|
|
426
443
|
return successResponse('close', { loop, next_expected: null }, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(loop));
|
|
427
444
|
});
|
|
428
445
|
}
|
|
446
|
+
case 'verify': {
|
|
447
|
+
// pln#632 — run the loop's opener-configured verify command + record a
|
|
448
|
+
// deterministic verify_report. runVerify manages its OWN two lock scopes (the
|
|
449
|
+
// spawn runs OUT of the lock), so it is NOT wrapped in withLockedLoopMutation.
|
|
450
|
+
const existing = getLoop(req.loop_id, options.cwd);
|
|
451
|
+
if (!existing) {
|
|
452
|
+
return errorResponse('verify', 'not_found', `unknown loop_id ${req.loop_id}`, Date.now() - startMs);
|
|
453
|
+
}
|
|
454
|
+
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
455
|
+
const result = runVerify({ loop_id: req.loop_id, actor }, options.cwd);
|
|
456
|
+
const newEvents = findNewLoopEvents(result.thread.id, beforeEvents, options.cwd);
|
|
457
|
+
const summary = result.unconfigured
|
|
458
|
+
? `verify: loop has no protocol.verify — falling back to an agent-narrated verify_report`
|
|
459
|
+
: result.deduped
|
|
460
|
+
? `verify: a verify_report already exists for iteration ${result.thread.iteration_count} (idempotent)`
|
|
461
|
+
: `${result.report?.passed ? '✔ verify green' : result.report?.timed_out ? '✖ verify RED (timeout)' : '✖ verify red'}: ${result.report?.command ?? ''}`;
|
|
462
|
+
return successResponse('verify', {
|
|
463
|
+
loop: result.thread,
|
|
464
|
+
verify_report: result.report ?? null,
|
|
465
|
+
deduped: result.deduped,
|
|
466
|
+
unconfigured: result.unconfigured ?? false,
|
|
467
|
+
next_expected: computeNextExpected(result.thread),
|
|
468
|
+
}, [loopArtifactEntry(result.thread.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', result.thread.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summary);
|
|
469
|
+
}
|
|
470
|
+
case 'bind': {
|
|
471
|
+
// pln#632 impl-loop bind — dispatch the loop's linked sequence + advance
|
|
472
|
+
// bind→execute. runImplBind awaits the async spawn (the advance takes its own
|
|
473
|
+
// lock via the verb), so it is NOT wrapped in withLockedLoopMutation — it mirrors
|
|
474
|
+
// coordinate(open_loop)'s async-handler-spawns pattern, not a synchronous verb.
|
|
475
|
+
const existing = getLoop(req.loop_id, options.cwd);
|
|
476
|
+
if (!existing) {
|
|
477
|
+
return errorResponse('bind', 'not_found', `unknown loop_id ${req.loop_id}`, Date.now() - startMs);
|
|
478
|
+
}
|
|
479
|
+
if (existing.kind !== 'implementation') {
|
|
480
|
+
return errorResponse('bind', 'validation_error', `bind is only valid for implementation loops (loop ${req.loop_id} is kind='${existing.kind}'); review/ideation loops dispatch via bclaw_coordinate`, Date.now() - startMs);
|
|
481
|
+
}
|
|
482
|
+
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
483
|
+
const bind = await runImplBind({
|
|
484
|
+
loop_id: req.loop_id,
|
|
485
|
+
dispatcherAgent: actor,
|
|
486
|
+
dispatcherAgentId: agentId,
|
|
487
|
+
sessionId: options.sessionId,
|
|
488
|
+
dryRun: req.dry_run,
|
|
489
|
+
lanes: req.lanes,
|
|
490
|
+
autoExecute: req.auto_execute,
|
|
491
|
+
model: req.model,
|
|
492
|
+
maxAssignments: req.max_assignments,
|
|
493
|
+
}, options.cwd);
|
|
494
|
+
const loop = getLoop(req.loop_id, options.cwd);
|
|
495
|
+
const newEvents = findNewLoopEvents(loop.id, beforeEvents, options.cwd);
|
|
496
|
+
const sideEffects = bind.action === 'bound'
|
|
497
|
+
? [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)]
|
|
498
|
+
: [...loopEventSideEffects(newEvents)];
|
|
499
|
+
return successResponse('bind', {
|
|
500
|
+
loop,
|
|
501
|
+
sequence_id: bind.sequence_id,
|
|
502
|
+
action: bind.action,
|
|
503
|
+
advanced_to: bind.advanced_to ?? null,
|
|
504
|
+
auto_closed: bind.auto_closed ?? false,
|
|
505
|
+
dispatched: bind.messages_sent,
|
|
506
|
+
dispatch: bind.dispatch,
|
|
507
|
+
next_expected: computeNextExpected(loop),
|
|
508
|
+
}, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], sideEffects, bind.dispatch?.warnings ?? [], Date.now() - startMs, bind.reason);
|
|
509
|
+
}
|
|
429
510
|
}
|
|
430
511
|
}
|
|
431
512
|
catch (err) {
|
|
@@ -360,19 +360,22 @@ export const MCP_READ_TOOLS = [
|
|
|
360
360
|
},
|
|
361
361
|
{
|
|
362
362
|
name: 'bclaw_read_inbox',
|
|
363
|
-
description: 'Read messages from an agent inbox.
|
|
363
|
+
description: 'Read messages from an agent inbox, newest-first. By default returns only ACTIONABLE messages (pending + read) and hides acknowledged/archived — pass includeAll=true, or a specific status, to widen. Message bodies are previewed (~500 chars) unless full=true; the page is size-bounded by budget_tokens so a read can never blow the token budget. Use markAsRead to auto-mark pending messages as read.',
|
|
364
364
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
365
365
|
inputSchema: {
|
|
366
366
|
type: 'object',
|
|
367
367
|
properties: {
|
|
368
368
|
agent: { type: 'string', description: 'Agent name whose inbox to read. Defaults to calling agent.' },
|
|
369
369
|
agentId: { type: 'string', description: 'Registered agent id.' },
|
|
370
|
-
status: { type: 'string', description: 'Filter by status: pending, read, acknowledged, archived.' },
|
|
370
|
+
status: { type: 'string', description: 'Filter by an exact status: pending, read, acknowledged, archived. Overrides the actionable default.' },
|
|
371
|
+
includeAll: { type: 'boolean', description: 'Return every status (including acknowledged + archived), disabling the actionable default. Default: false.' },
|
|
371
372
|
type: { type: 'string', description: 'Filter by message type: assign, review, rfc, info, reply.' },
|
|
372
373
|
thread_id: { type: 'string', description: 'Filter by thread ID to see a conversation.' },
|
|
374
|
+
full: { type: 'boolean', description: 'Return complete message bodies instead of ~500-char previews. Each previewed message reports text_length + truncated:true. Default: false.' },
|
|
373
375
|
markAsRead: { type: 'boolean', description: 'Mark pending messages as read. Default: false.' },
|
|
374
376
|
limit: { type: 'number', description: 'Maximum messages to return (default: 20).' },
|
|
375
377
|
offset: { type: 'number', description: 'Skip N messages for pagination.' },
|
|
378
|
+
budget_tokens: { type: 'number', description: 'Cap the returned page size (~4 chars/token). Messages are trimmed until the payload fits; has_more/next_offset let you page the rest.' },
|
|
376
379
|
},
|
|
377
380
|
},
|
|
378
381
|
},
|
|
@@ -1028,8 +1031,8 @@ const MCP_WRITE_TOOLS = [
|
|
|
1028
1031
|
// created a loop structure without dispatching the first turn, so
|
|
1029
1032
|
// nothing ever ran. Loops are opened via
|
|
1030
1033
|
// bclaw_coordinate(intent='review', open_loop=true) or intent='ideate'.
|
|
1031
|
-
enum: ['get', 'list', 'turn', 'complete_turn', 'advance', 'add_artifact', 'pause', 'resume', 'close'],
|
|
1032
|
-
description: 'Loop lifecycle intent for driving turns inside a loop that was already opened via the coordinate facade. To START a loop, use `bclaw_coordinate(intent="review", open_loop=true, targetAgents=[…])` or `intent="ideate"` — that opens the loop AND dispatches the first turn. See docs/concepts/loop-engine.md.',
|
|
1034
|
+
enum: ['get', 'list', 'turn', 'complete_turn', 'advance', 'add_artifact', 'pause', 'resume', 'close', 'bind'],
|
|
1035
|
+
description: 'Loop lifecycle intent for driving turns inside a loop that was already opened via the coordinate facade. To START a loop, use `bclaw_coordinate(intent="review", open_loop=true, targetAgents=[…])` or `intent="ideate"` — that opens the loop AND dispatches the first turn. `bind` (implementation loops only) dispatches the loop\'s linked sequence and advances bind→execute — the engine action for the `bind` phase. See docs/concepts/loop-engine.md.',
|
|
1033
1036
|
},
|
|
1034
1037
|
loop_id: { type: 'string', description: 'Target loop id (lop_…). Required for every intent except open and list.' },
|
|
1035
1038
|
kind: { type: 'string', enum: ['review', 'ideation', 'implementation', 'research', 'debug'], description: 'Loop kind for open / list filter.' },
|
|
@@ -1052,6 +1055,11 @@ const MCP_WRITE_TOOLS = [
|
|
|
1052
1055
|
outcome: { type: 'string', enum: ['done', 'failed', 'cancelled'], description: 'complete_turn outcome (default done).' },
|
|
1053
1056
|
failure_reason: { type: 'string', description: 'complete_turn: optional failure/cancel reason.' },
|
|
1054
1057
|
artifact: { type: 'object', description: 'complete_turn / add_artifact payload: { phase, type, body?, produced_by?, ref? }.' },
|
|
1058
|
+
dry_run: { type: 'boolean', description: 'bind: analyze + report what would dispatch; no spawn, no advance.' },
|
|
1059
|
+
lanes: { type: 'array', items: { type: 'string' }, description: 'bind: restrict the dispatch to specific sequence lanes.' },
|
|
1060
|
+
auto_execute: { type: 'boolean', description: 'bind: deliver briefs without spawning (→ manual launch commands).' },
|
|
1061
|
+
model: { type: 'string', description: 'bind: model override for the dispatched agents.' },
|
|
1062
|
+
max_assignments: { type: 'number', description: 'bind: cap assignments made in this bind.' },
|
|
1055
1063
|
to_phase: { type: 'string', description: 'advance: explicit target phase (otherwise the next phase).' },
|
|
1056
1064
|
force: { type: 'boolean', description: 'advance: allow going backwards (increments iteration_count).' },
|
|
1057
1065
|
reason: { type: 'string', description: 'advance / pause / close: optional reason string.' },
|
|
@@ -149,6 +149,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
149
149
|
let agentNameCache;
|
|
150
150
|
return dispatchReadTool(name, args, {
|
|
151
151
|
cwd,
|
|
152
|
+
sourceCwd: effective.cwd,
|
|
152
153
|
activeSource,
|
|
153
154
|
resolvedProject,
|
|
154
155
|
projectRoutingApplied,
|
|
@@ -1803,6 +1804,8 @@ function dispatchReadTool(name, args, ctx) {
|
|
|
1803
1804
|
if (name === 'bclaw_read_inbox') {
|
|
1804
1805
|
const agentName = args.agent ?? ctx.getAgentName();
|
|
1805
1806
|
const markAsRead = args.markAsRead === true; // default: false — reading doesn't imply processing
|
|
1807
|
+
const includeAll = args.includeAll === true; // pln#627 Phase A — widen past the actionable default
|
|
1808
|
+
const full = args.full === true; // pln#627 Phase A — return whole bodies, not previews
|
|
1806
1809
|
const result = readInbox({
|
|
1807
1810
|
agent: agentName,
|
|
1808
1811
|
status: args.status,
|
|
@@ -1810,21 +1813,77 @@ function dispatchReadTool(name, args, ctx) {
|
|
|
1810
1813
|
thread_id: args.thread_id,
|
|
1811
1814
|
limit: args.limit,
|
|
1812
1815
|
offset: args.offset,
|
|
1816
|
+
includeAll,
|
|
1813
1817
|
markAsRead,
|
|
1814
1818
|
}, cwd);
|
|
1815
|
-
|
|
1816
|
-
|
|
1819
|
+
// pln#627 Phase A — bound the payload so a single inbox read can never blow
|
|
1820
|
+
// the MCP token budget (root cause: persona/CoDev dumps persisted as inbox
|
|
1821
|
+
// messages, one at 960 KB). Two independent guards:
|
|
1822
|
+
// 1. per-message: preview each body to INBOX_PREVIEW_CHARS unless full=true;
|
|
1823
|
+
// the whole body stays available via full=true (bclaw_get(inbox_message)
|
|
1824
|
+
// is per-agent-scoped and cannot serve one message by id).
|
|
1825
|
+
// 2. whole-page: boundListResult trims messages until the JSON fits the
|
|
1826
|
+
// char budget, the same way bclaw_find / bclaw_search do (~4 chars/token).
|
|
1827
|
+
const INBOX_PREVIEW_CHARS = 500;
|
|
1828
|
+
const projected = result.messages.map((msg) => {
|
|
1829
|
+
const textLength = msg.text.length;
|
|
1830
|
+
const truncated = !full && textLength > INBOX_PREVIEW_CHARS;
|
|
1831
|
+
return {
|
|
1832
|
+
...msg,
|
|
1833
|
+
text: truncated ? msg.text.slice(0, INBOX_PREVIEW_CHARS) : msg.text,
|
|
1834
|
+
text_length: textLength,
|
|
1835
|
+
truncated,
|
|
1836
|
+
};
|
|
1837
|
+
});
|
|
1838
|
+
const budgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
|
|
1839
|
+
const charBudget = budgetTokens ? Math.min(budgetTokens * 4, DEFAULT_FIND_CHAR_BUDGET) : DEFAULT_FIND_CHAR_BUDGET;
|
|
1840
|
+
const bounded = boundListResult({ entity: 'inbox_message', total: result.total, items: projected }, result.offset, charBudget);
|
|
1841
|
+
const scopeNote = includeAll || args.status ? '' : ' actionable (pending+read); pass includeAll=true for acknowledged/archived';
|
|
1842
|
+
const lines = [`Inbox for ${agentName} — ${result.total} message(s)${scopeNote}:`];
|
|
1843
|
+
for (const msg of bounded.items) {
|
|
1817
1844
|
const ack = msg.requires_ack ? ' [ACK required]' : '';
|
|
1818
1845
|
const thread = msg.thread_id ? ` thread:${msg.thread_id}` : '';
|
|
1819
1846
|
lines.push(` [${msg.short_label ?? msg.id}] ${msg.type} from ${msg.from} (${msg.status})${ack}${thread}`);
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1847
|
+
const preview = msg.text.slice(0, 200);
|
|
1848
|
+
const more = msg.truncated || msg.text_length > 200;
|
|
1849
|
+
const moreNote = more
|
|
1850
|
+
? `… (${msg.text_length} chars${msg.truncated ? '; pass full=true for the whole body' : ''})`
|
|
1851
|
+
: '';
|
|
1852
|
+
lines.push(` ${preview}${moreNote}`);
|
|
1853
|
+
}
|
|
1854
|
+
if (bounded.items.length === 0) {
|
|
1823
1855
|
lines.push(' (no messages)');
|
|
1824
1856
|
}
|
|
1857
|
+
if (bounded.hint)
|
|
1858
|
+
lines.push('', bounded.hint);
|
|
1859
|
+
const nextActions = bounded.has_more
|
|
1860
|
+
? [{
|
|
1861
|
+
tool: 'bclaw_read_inbox',
|
|
1862
|
+
args: {
|
|
1863
|
+
...(args.agent ? { agent: args.agent } : {}),
|
|
1864
|
+
offset: bounded.next_offset,
|
|
1865
|
+
...(args.limit ? { limit: args.limit } : {}),
|
|
1866
|
+
...(args.status ? { status: args.status } : {}),
|
|
1867
|
+
...(includeAll ? { includeAll: true } : {}),
|
|
1868
|
+
},
|
|
1869
|
+
when: 'to fetch the next page',
|
|
1870
|
+
}]
|
|
1871
|
+
: [];
|
|
1825
1872
|
return {
|
|
1826
1873
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
1827
|
-
structuredContent: {
|
|
1874
|
+
structuredContent: {
|
|
1875
|
+
total: result.total,
|
|
1876
|
+
offset: result.offset,
|
|
1877
|
+
limit: result.limit,
|
|
1878
|
+
messages: bounded.items,
|
|
1879
|
+
returned: bounded.returned,
|
|
1880
|
+
has_more: bounded.has_more,
|
|
1881
|
+
...(bounded.next_offset !== undefined ? { next_offset: bounded.next_offset } : {}),
|
|
1882
|
+
...(bounded.omitted_for_size ? { omitted_for_size: bounded.omitted_for_size } : {}),
|
|
1883
|
+
...(bounded.hint ? { hint: bounded.hint } : {}),
|
|
1884
|
+
...(nextActions.length ? { next_actions: nextActions } : {}),
|
|
1885
|
+
schema_version: SCHEMA_VERSION,
|
|
1886
|
+
},
|
|
1828
1887
|
};
|
|
1829
1888
|
}
|
|
1830
1889
|
if (name === 'bclaw_context') {
|
|
@@ -1915,9 +1974,33 @@ function dispatchReadTool(name, args, ctx) {
|
|
|
1915
1974
|
` stderr: ${status.runtime.log_files.stderr?.exists ? `${status.runtime.log_files.stderr.size_bytes}B` : 'absent'}`,
|
|
1916
1975
|
` git: commits_ahead=${status.runtime.commits_ahead ?? 'n/a'} dirty_tracked=${status.runtime.dirty_tracked ?? 'n/a'}`,
|
|
1917
1976
|
];
|
|
1977
|
+
// pln#521 P1 (B4) — routing echo. Operators debugging a dispatch need to see
|
|
1978
|
+
// which project this status was read from, and WHY that project won, without
|
|
1979
|
+
// reverse-engineering cwd + store state. The decision (project_name/
|
|
1980
|
+
// project_cwd) is a first-class field; the reasoning is the `_resolution_trace`
|
|
1981
|
+
// sibling, which by design ships here and nowhere else. Deliberately cheap:
|
|
1982
|
+
// no candidate/nested-store scan on a hot read path. source_cwd is the
|
|
1983
|
+
// PRE-routing cwd (ctx.sourceCwd) — using the routed cwd would make the two
|
|
1984
|
+
// ends identical exactly when a `project` arg hopped stores, i.e. the one
|
|
1985
|
+
// case the trace exists to show.
|
|
1986
|
+
const projectCwd = resolvedProject?.path ?? cwd;
|
|
1987
|
+
const projectName = resolvedProject?.name;
|
|
1988
|
+
const resolutionTrace = {
|
|
1989
|
+
source_cwd: ctx.sourceCwd,
|
|
1990
|
+
effective_cwd: projectCwd,
|
|
1991
|
+
active_source: activeSource,
|
|
1992
|
+
...(projectRoutingApplied && typeof args.project === 'string' ? { project_arg: args.project } : {}),
|
|
1993
|
+
};
|
|
1994
|
+
lines.push('', `Project: ${projectName ?? '(unnamed)'} — ${projectCwd} (via ${activeSource})`);
|
|
1918
1995
|
return {
|
|
1919
1996
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
1920
|
-
structuredContent: {
|
|
1997
|
+
structuredContent: {
|
|
1998
|
+
...status,
|
|
1999
|
+
project_cwd: projectCwd,
|
|
2000
|
+
...(projectName ? { project_name: projectName } : {}),
|
|
2001
|
+
_resolution_trace: resolutionTrace,
|
|
2002
|
+
schema_version: SCHEMA_VERSION,
|
|
2003
|
+
},
|
|
1921
2004
|
};
|
|
1922
2005
|
}
|
|
1923
2006
|
throw new Error(`Unknown read tool: ${name}`);
|