@cello-protocol/daemon 0.0.47 → 0.0.49
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/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +119 -41
- package/dist/daemon.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/vocabulary.d.ts +122 -0
- package/dist/vocabulary.d.ts.map +1 -0
- package/dist/vocabulary.js +270 -0
- package/dist/vocabulary.js.map +1 -0
- package/package.json +2 -2
package/dist/daemon.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAU/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAkhM7E"}
|
package/dist/daemon.js
CHANGED
|
@@ -31,6 +31,7 @@ import { dirname, join } from "node:path";
|
|
|
31
31
|
import { loadAgents } from "./agent-loader.js";
|
|
32
32
|
import { acquireLock, removeLock } from "./lock-file.js";
|
|
33
33
|
import { createIpcServer } from "./ipc-server.js";
|
|
34
|
+
import { renderForSurface } from "./vocabulary.js";
|
|
34
35
|
import { SessionNodeManager } from "./session-node-manager.js";
|
|
35
36
|
import { TIER, isKnownTierValue } from "./contacts-tier-migration.js";
|
|
36
37
|
import { isValidSettingKey, allSettingKeys, validateSettingValue, AWAY_MESSAGE_MAX_LEN } from "./agent-settings-keys.js";
|
|
@@ -1797,7 +1798,7 @@ export async function startDaemon(config) {
|
|
|
1797
1798
|
function startAgentInternal(name) {
|
|
1798
1799
|
const agent = agents.find((a) => a.name === name);
|
|
1799
1800
|
if (!agent || agent.state === "load_failed") {
|
|
1800
|
-
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}' does not exist. Run 'cello login' to register agents, or check agent names with
|
|
1801
|
+
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}' does not exist. Run 'cello login' to register agents, or check agent names with cello_agents.` };
|
|
1801
1802
|
}
|
|
1802
1803
|
if (onlineAgents.has(name)) {
|
|
1803
1804
|
// Idempotent — already online, no event
|
|
@@ -1874,7 +1875,7 @@ export async function startDaemon(config) {
|
|
|
1874
1875
|
}
|
|
1875
1876
|
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
1876
1877
|
if (store.hasActiveAgent(name) || agents.some((a) => a.name === name)) {
|
|
1877
|
-
return { ok: false, reason: "agent_already_exists", guidance: `Agent '${name}' already exists. Choose a different name, or see
|
|
1878
|
+
return { ok: false, reason: "agent_already_exists", guidance: `Agent '${name}' already exists. Choose a different name, or see cello_agents.` };
|
|
1878
1879
|
}
|
|
1879
1880
|
let pubkeyHex;
|
|
1880
1881
|
let agentId;
|
|
@@ -1984,7 +1985,7 @@ export async function startDaemon(config) {
|
|
|
1984
1985
|
// any retire — the active-only accessors filter retired rows out once state is flipped.
|
|
1985
1986
|
const target = store.getAgentForRevocation(name);
|
|
1986
1987
|
if (!target) {
|
|
1987
|
-
return { ok: false, reason: "agent_not_found", guidance: `No agent named '${name}'. Check
|
|
1988
|
+
return { ok: false, reason: "agent_not_found", guidance: `No agent named '${name}'. Check cello_agents.` };
|
|
1988
1989
|
}
|
|
1989
1990
|
const wasActive = target.state !== "retired";
|
|
1990
1991
|
const agentId = target.localAgentId;
|
|
@@ -2082,7 +2083,7 @@ export async function startDaemon(config) {
|
|
|
2082
2083
|
}
|
|
2083
2084
|
const agent = agents.find((a) => a.name === name);
|
|
2084
2085
|
if (!agent || agent.state === "load_failed") {
|
|
2085
|
-
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}' does not exist. Check agent names with
|
|
2086
|
+
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}' does not exist. Check agent names with cello_agents.` };
|
|
2086
2087
|
}
|
|
2087
2088
|
if (!onlineAgents.has(name)) {
|
|
2088
2089
|
// Idempotent — already registered/offline, no event
|
|
@@ -2113,7 +2114,7 @@ export async function startDaemon(config) {
|
|
|
2113
2114
|
}
|
|
2114
2115
|
const agent = agents.find((a) => a.name === name);
|
|
2115
2116
|
if (!agent || agent.state === "load_failed") {
|
|
2116
|
-
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}' does not exist. Create it with 'cello create-agent ${name}', register it, then retry — or check names with
|
|
2117
|
+
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}' does not exist. Create it with 'cello create-agent ${name}', register it, then retry — or check names with cello_agents.` };
|
|
2117
2118
|
}
|
|
2118
2119
|
const connState = perConnectionState.get(connectionId);
|
|
2119
2120
|
if (!connState) {
|
|
@@ -2166,7 +2167,7 @@ export async function startDaemon(config) {
|
|
|
2166
2167
|
const reg = await new DbRegistrationPersistence({ db: sessionNodeManager.getDb(), agentName: name, logger }).loadRegistrationState();
|
|
2167
2168
|
if (!reg || reg.status !== "active") {
|
|
2168
2169
|
result["warning"] = "not_registered";
|
|
2169
|
-
result["warning_guidance"] = `Agent '${name}' is now selected but is not registered with the directory — run 'cello register ${name}' to enable sessions with peers. Run 'cello status' to watch registration complete.`;
|
|
2170
|
+
result["warning_guidance"] = `Agent '${name}' is now selected but is not registered with the directory — run 'cello register-agent ${name}' to enable sessions with peers. Run 'cello status' to watch registration complete.`;
|
|
2170
2171
|
}
|
|
2171
2172
|
}
|
|
2172
2173
|
catch (err) {
|
|
@@ -2219,11 +2220,11 @@ export async function startDaemon(config) {
|
|
|
2219
2220
|
return { ok: false, reason: "missing_params", guidance: "Provide 'agent' (the agent name to register) and 'preAuthToken' (the pre-authorization ticket from the CELLO Operations Agent)." };
|
|
2220
2221
|
}
|
|
2221
2222
|
if (!preAuthToken) {
|
|
2222
|
-
return { ok: false, reason: "missing_preauth_token", guidance: "Registration requires a 'preAuthToken' issued by the CELLO Operations Agent (Telegram). Obtain one, then retry
|
|
2223
|
+
return { ok: false, reason: "missing_preauth_token", guidance: "Registration requires a 'preAuthToken' issued by the CELLO Operations Agent (Telegram). Obtain one, then retry 'cello register-agent'." };
|
|
2223
2224
|
}
|
|
2224
2225
|
const keyProvider = keyProviders.get(name);
|
|
2225
2226
|
if (!keyProvider) {
|
|
2226
|
-
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}' does not exist. Create it first with
|
|
2227
|
+
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}' does not exist. Create it first with 'cello create-agent ${name}', then retry 'cello register-agent'.` };
|
|
2227
2228
|
}
|
|
2228
2229
|
if (!directoryEndpointResolver) {
|
|
2229
2230
|
return { ok: false, reason: "directory_unreachable", guidance: "The daemon has no directory endpoint resolver configured, so it cannot reach the directory to register." };
|
|
@@ -2273,7 +2274,7 @@ export async function startDaemon(config) {
|
|
|
2273
2274
|
return {
|
|
2274
2275
|
ok: false,
|
|
2275
2276
|
reason: "directory_signaling_timeout",
|
|
2276
|
-
guidance: `Agent '${name}' could not establish its directory signaling stream within 10s. Check CELLO_DIRECTORY_URL and that the directory is reachable, then retry
|
|
2277
|
+
guidance: `Agent '${name}' could not establish its directory signaling stream within 10s. Check CELLO_DIRECTORY_URL and that the directory is reachable, then retry 'cello register-agent'.`,
|
|
2277
2278
|
};
|
|
2278
2279
|
}
|
|
2279
2280
|
const persistence = getPersistence(name);
|
|
@@ -2903,9 +2904,13 @@ export async function startDaemon(config) {
|
|
|
2903
2904
|
// NOTE: cello_await_session is NOT in this stub list — Seam 2 registers a real
|
|
2904
2905
|
// handler for it below (inbound session establishment), with its own inline
|
|
2905
2906
|
// no_current_agent guard.
|
|
2906
|
-
//
|
|
2907
|
-
// alias
|
|
2908
|
-
//
|
|
2907
|
+
// DOD-ONBOARD-HELP-1: cello_receive_session is DELETED (Andre, 2026-07-11). It was a literal
|
|
2908
|
+
// alias — the same handler object as cello_receive — that claimed an "accept/join" step CELLO
|
|
2909
|
+
// does not have (inbound sessions are auto-accepted by the standing receiver). No alias, no
|
|
2910
|
+
// deprecated shim, no dead handler.
|
|
2911
|
+
//
|
|
2912
|
+
// The list below stays as an (empty) extension point: a future tool that needs only the plain
|
|
2913
|
+
// no_current_agent guard, with no handler of its own, is registered here.
|
|
2909
2914
|
const SESSION_TOOLS_REQUIRING_AGENT = [];
|
|
2910
2915
|
const NO_CURRENT_AGENT_RESPONSE = {
|
|
2911
2916
|
ok: false,
|
|
@@ -3092,7 +3097,7 @@ export async function startDaemon(config) {
|
|
|
3092
3097
|
return {
|
|
3093
3098
|
ok: false,
|
|
3094
3099
|
reason: "session_not_found",
|
|
3095
|
-
guidance: "No session found with this ID. Check
|
|
3100
|
+
guidance: "No session found with this ID. Check cello_sessions for active and interrupted sessions.",
|
|
3096
3101
|
};
|
|
3097
3102
|
}
|
|
3098
3103
|
// Ownership: redundant now that the lookup is agent-scoped (record.agent_name === currentAgent),
|
|
@@ -3101,7 +3106,7 @@ export async function startDaemon(config) {
|
|
|
3101
3106
|
return {
|
|
3102
3107
|
ok: false,
|
|
3103
3108
|
reason: "session_not_owned",
|
|
3104
|
-
guidance: "This session belongs to a different agent. Call cello_use_agent to switch to the agent that owns it (see
|
|
3109
|
+
guidance: "This session belongs to a different agent. Call cello_use_agent to switch to the agent that owns it (see cello_sessions), then retry.",
|
|
3105
3110
|
};
|
|
3106
3111
|
}
|
|
3107
3112
|
// AC-010: already sealed
|
|
@@ -3109,7 +3114,7 @@ export async function startDaemon(config) {
|
|
|
3109
3114
|
return {
|
|
3110
3115
|
ok: false,
|
|
3111
3116
|
reason: "session_already_sealed",
|
|
3112
|
-
guidance: "This session is already sealed. No further action is needed — check
|
|
3117
|
+
guidance: "This session is already sealed. No further action is needed — check cello_sessions to view its sealed record and the FROST notarization.",
|
|
3113
3118
|
};
|
|
3114
3119
|
}
|
|
3115
3120
|
// CC-5/F21 terminal-escape: force-abandon a session that can never be bilaterally sealed — a
|
|
@@ -3354,7 +3359,7 @@ export async function startDaemon(config) {
|
|
|
3354
3359
|
return {
|
|
3355
3360
|
ok: false,
|
|
3356
3361
|
reason: uniResult.reason,
|
|
3357
|
-
guidance: "The unilateral seal did not complete (the directory could not verify the reported root, or the certificate failed verification). Confirm your messages reached the relay (
|
|
3362
|
+
guidance: "The unilateral seal did not complete (the directory could not verify the reported root, or the certificate failed verification). Confirm your messages reached the relay (cello_sessions) before retrying cello_close_session.",
|
|
3358
3363
|
};
|
|
3359
3364
|
}
|
|
3360
3365
|
finally {
|
|
@@ -3374,7 +3379,7 @@ export async function startDaemon(config) {
|
|
|
3374
3379
|
return {
|
|
3375
3380
|
ok: false,
|
|
3376
3381
|
reason: "session_not_closeable",
|
|
3377
|
-
guidance: `Session is in status '${record.status}', which cannot be closed via cello_close_session. Check
|
|
3382
|
+
guidance: `Session is in status '${record.status}', which cannot be closed via cello_close_session. Check cello_sessions; a seal_interrupted_pending session is awaiting FROST notarization.`,
|
|
3378
3383
|
};
|
|
3379
3384
|
});
|
|
3380
3385
|
// ─── MCP-001: stubs for tools registered in cello-mcp.ts but not yet implemented ───
|
|
@@ -3396,7 +3401,7 @@ export async function startDaemon(config) {
|
|
|
3396
3401
|
// cello-mcp forwards this as { session_id } (snake_case, matching the other session tools).
|
|
3397
3402
|
const sessionId = params?.["session_id"];
|
|
3398
3403
|
if (!sessionId || typeof sessionId !== "string") {
|
|
3399
|
-
return { ok: false, reason: "missing_session_id", guidance: "Provide the session_id (hex) of the sealed session. Check
|
|
3404
|
+
return { ok: false, reason: "missing_session_id", guidance: "Provide the session_id (hex) of the sealed session. Check cello_sessions for sealed sessions." };
|
|
3400
3405
|
}
|
|
3401
3406
|
// DOD-LOOP-1: the certificate is keyed by (agent, session_id) — read the current agent's.
|
|
3402
3407
|
const connState = perConnectionState.get(connectionId);
|
|
@@ -3428,7 +3433,7 @@ export async function startDaemon(config) {
|
|
|
3428
3433
|
return {
|
|
3429
3434
|
ok: false,
|
|
3430
3435
|
reason: "wrong_agent",
|
|
3431
|
-
guidance: `This session belongs to agent '${owner.name}', not the current agent '${agentName}'. Call cello_use_agent('${owner.name}') then retry
|
|
3436
|
+
guidance: `This session belongs to agent '${owner.name}', not the current agent '${agentName}'. Call cello_use_agent('${owner.name}') then retry cello_sealed_receipt.`,
|
|
3432
3437
|
};
|
|
3433
3438
|
}
|
|
3434
3439
|
// A truncated paste: the id is a strict prefix of one of this agent's real session ids.
|
|
@@ -3439,13 +3444,13 @@ export async function startDaemon(config) {
|
|
|
3439
3444
|
return {
|
|
3440
3445
|
ok: false,
|
|
3441
3446
|
reason: "session_id_too_short",
|
|
3442
|
-
guidance: "That looks like a truncated session id.
|
|
3447
|
+
guidance: "That looks like a truncated session id. cello_sessions and cello status show the FULL id — copy the complete id and retry.",
|
|
3443
3448
|
};
|
|
3444
3449
|
}
|
|
3445
3450
|
return {
|
|
3446
3451
|
ok: false,
|
|
3447
3452
|
reason: "unknown_session",
|
|
3448
|
-
guidance: "No session with this id belongs to the current agent. Run
|
|
3453
|
+
guidance: "No session with this id belongs to the current agent. Run cello_sessions to see the full ids of this agent's sessions.",
|
|
3449
3454
|
};
|
|
3450
3455
|
});
|
|
3451
3456
|
// DOD-LOG-1 (PERSIST-LOG-001): read the durable, decrypted conversation transcript for a session —
|
|
@@ -3455,7 +3460,7 @@ export async function startDaemon(config) {
|
|
|
3455
3460
|
handlers.set("cello_get_transcript", async (params, connectionId) => {
|
|
3456
3461
|
const sessionId = params?.["session_id"];
|
|
3457
3462
|
if (!sessionId || typeof sessionId !== "string") {
|
|
3458
|
-
return { ok: false, reason: "missing_session_id", guidance: "Provide the session_id (hex) whose transcript to read. See
|
|
3463
|
+
return { ok: false, reason: "missing_session_id", guidance: "Provide the session_id (hex) whose transcript to read. See cello_sessions." };
|
|
3459
3464
|
}
|
|
3460
3465
|
const connState = perConnectionState.get(connectionId);
|
|
3461
3466
|
// CC-3 / M8C-AUTOSTART-1 F18: explicit { name } > this connection's current > sole online agent.
|
|
@@ -4743,7 +4748,7 @@ export async function startDaemon(config) {
|
|
|
4743
4748
|
return {
|
|
4744
4749
|
ok: false,
|
|
4745
4750
|
reason: "seal_interrupted_counterparty_unavailable",
|
|
4746
|
-
guidance: "The counterparty is not currently reachable to complete the seal-interrupted flow. Retry when the counterparty is online — check their connection status via
|
|
4751
|
+
guidance: "The counterparty is not currently reachable to complete the seal-interrupted flow. Retry when the counterparty is online — check their connection status via cello_status.",
|
|
4747
4752
|
};
|
|
4748
4753
|
}
|
|
4749
4754
|
// Wait for counterparty ack/rejection via the shared signaling await machinery.
|
|
@@ -4759,7 +4764,7 @@ export async function startDaemon(config) {
|
|
|
4759
4764
|
return {
|
|
4760
4765
|
ok: false,
|
|
4761
4766
|
reason: "seal_interrupted_counterparty_unavailable",
|
|
4762
|
-
guidance: "The counterparty is not currently reachable to complete the seal-interrupted flow. Retry when the counterparty is online — check their connection status via
|
|
4767
|
+
guidance: "The counterparty is not currently reachable to complete the seal-interrupted flow. Retry when the counterparty is online — check their connection status via cello_status.",
|
|
4763
4768
|
};
|
|
4764
4769
|
}
|
|
4765
4770
|
if (ackResult.type === "seal_interrupted_rejection") {
|
|
@@ -4955,7 +4960,7 @@ export async function startDaemon(config) {
|
|
|
4955
4960
|
ok: false,
|
|
4956
4961
|
reason,
|
|
4957
4962
|
guidance: ackResult.type === "timeout"
|
|
4958
|
-
? "The counterparty did not acknowledge the seal in time. Retry when they are online — check
|
|
4963
|
+
? "The counterparty did not acknowledge the seal in time. Retry when they are online — check cello_status. The session remains active and usable."
|
|
4959
4964
|
: "The counterparty rejected the seal request. Their session state may be inconsistent. Ask them to check cello status before retrying.",
|
|
4960
4965
|
};
|
|
4961
4966
|
}
|
|
@@ -5030,7 +5035,7 @@ export async function startDaemon(config) {
|
|
|
5030
5035
|
// DOD-LOOP-1: the (agent, session_id) lookup is itself the ownership scope.
|
|
5031
5036
|
const record = sessionNodeManager.getSessionRecord(agentName, sessionId);
|
|
5032
5037
|
if (!record) {
|
|
5033
|
-
return { ok: false, reason: "session_not_found", guidance: "No session found with this ID. Check
|
|
5038
|
+
return { ok: false, reason: "session_not_found", guidance: "No session found with this ID. Check cello_sessions for active sessions." };
|
|
5034
5039
|
}
|
|
5035
5040
|
if (record.agent_name !== agentName) {
|
|
5036
5041
|
return { ok: false, reason: "session_not_owned", guidance: "This session belongs to a different agent. Call cello_use_agent to switch to the agent that owns it, then retry." };
|
|
@@ -5091,7 +5096,12 @@ export async function startDaemon(config) {
|
|
|
5091
5096
|
current_seq: currentSeq,
|
|
5092
5097
|
last_read_seq: connectionCursor,
|
|
5093
5098
|
unread_received: unreadReceived,
|
|
5094
|
-
|
|
5099
|
+
// DOD-ONBOARD-HELP-1 §5: say what is wrong and what to do, in that order, in plain words.
|
|
5100
|
+
// The old text opened with the internal noun ("this agent hasn't read…"); a refused send is
|
|
5101
|
+
// the most common wall a new operator hits, so it leads with the COUNT and the FIX. The
|
|
5102
|
+
// session id is interpolated so the remedy is copy-pasteable, not a template to fill in.
|
|
5103
|
+
// Rendered per surface at the IPC boundary — a CLI caller sees `cello receive <id>`.
|
|
5104
|
+
guidance: `${unreadReceived} unread message(s) — run cello_receive ${sessionId} first to read them, then send again. (Or cello_transcript ${sessionId} for the whole conversation.) You are blocked from replying to something you haven't read.`,
|
|
5095
5105
|
};
|
|
5096
5106
|
}
|
|
5097
5107
|
// M9-FEED-001 §6: the agent's governance re-send decisions, keyed by the flagId a prior `warn`
|
|
@@ -5214,7 +5224,7 @@ export async function startDaemon(config) {
|
|
|
5214
5224
|
return {
|
|
5215
5225
|
ok: false,
|
|
5216
5226
|
reason: sendResult.reason,
|
|
5217
|
-
guidance: "The content could not be delivered over the session stream right now. It has been queued in the durable retry queue and will be retried when the counterparty reconnects. The session remains usable — check
|
|
5227
|
+
guidance: "The content could not be delivered over the session stream right now. It has been queued in the durable retry queue and will be retried when the counterparty reconnects. The session remains usable — check cello_status for the counterparty's status.",
|
|
5218
5228
|
};
|
|
5219
5229
|
}
|
|
5220
5230
|
// Delivered directly OR dispatched to relay (DOD-LEAVEMSG-1) — either way the content is now
|
|
@@ -5286,11 +5296,14 @@ export async function startDaemon(config) {
|
|
|
5286
5296
|
});
|
|
5287
5297
|
// ─── CELLO-M7-DAEMON-004 / F1-a: cello_receive (BLOCKING, session-scoped) ────────
|
|
5288
5298
|
// F1-a fix: the daemon port had dropped the blocking receive (the handler was a
|
|
5289
|
-
// non-blocking buf.shift
|
|
5290
|
-
//
|
|
5291
|
-
//
|
|
5292
|
-
//
|
|
5293
|
-
// (
|
|
5299
|
+
// non-blocking buf.shift). It now BLOCKS up to timeout_ms, polling the received-content
|
|
5300
|
+
// buffer — resolved by the next arrival, a terminal seal answer (F1-b), or timeout. This is
|
|
5301
|
+
// the "blocking receive variant" the guidance names.
|
|
5302
|
+
//
|
|
5303
|
+
// ONE name (DOD-ONBOARD-HELP-1, Andre 2026-07-11). This was briefly registered under
|
|
5304
|
+
// `cello_receive_session` as well — one implementation behind two names. That alias is DELETED:
|
|
5305
|
+
// it accepted or joined nothing (inbound sessions are auto-accepted by the standing receiver),
|
|
5306
|
+
// so it was a second name for a step that does not exist, and its help said otherwise.
|
|
5294
5307
|
const RECEIVE_DEFAULT_TIMEOUT_MS = 30000; // matches the cello-mcp shim's documented default
|
|
5295
5308
|
const handleReceive = async (params, connectionId) => {
|
|
5296
5309
|
const connState = perConnectionState.get(connectionId);
|
|
@@ -5313,7 +5326,7 @@ export async function startDaemon(config) {
|
|
|
5313
5326
|
let transcriptOnly = false;
|
|
5314
5327
|
if (!record) {
|
|
5315
5328
|
if (sessionNodeManager.readTranscript(agentName, sessionId).messages.length === 0) {
|
|
5316
|
-
return { ok: false, reason: "session_not_found", guidance: "No session found with this ID. Check
|
|
5329
|
+
return { ok: false, reason: "session_not_found", guidance: "No session found with this ID. Check cello_sessions." };
|
|
5317
5330
|
}
|
|
5318
5331
|
transcriptOnly = true;
|
|
5319
5332
|
}
|
|
@@ -5360,7 +5373,7 @@ export async function startDaemon(config) {
|
|
|
5360
5373
|
return {
|
|
5361
5374
|
ok: false,
|
|
5362
5375
|
reason: "session_not_live",
|
|
5363
|
-
guidance: "This session exists only as a durable transcript (no live session — it was never established or predates this daemon). Read it with cello_receive { since_seq } (e.g. since_seq: -1 for everything) or
|
|
5376
|
+
guidance: "This session exists only as a durable transcript (no live session — it was never established or predates this daemon). Read it with cello_receive { since_seq } (e.g. since_seq: -1 for everything) or cello_transcript.",
|
|
5364
5377
|
};
|
|
5365
5378
|
}
|
|
5366
5379
|
const rawTimeout = params?.timeout_ms;
|
|
@@ -5401,8 +5414,8 @@ export async function startDaemon(config) {
|
|
|
5401
5414
|
...(sealedRoot ? { sealed_root: sealedRoot } : {}),
|
|
5402
5415
|
unread_count: terminal.unreadCount,
|
|
5403
5416
|
guidance: terminal.unreadCount > 0
|
|
5404
|
-
? `The session has been sealed by both parties. ${terminal.unreadCount} message(s) arrived that were not read live — call
|
|
5405
|
-
: "The session has been sealed by both parties. The full history is available via
|
|
5417
|
+
? `The session has been sealed by both parties. ${terminal.unreadCount} message(s) arrived that were not read live — call cello_transcript to retrieve the full sealed history. No further actions are required on this session.`
|
|
5418
|
+
: "The session has been sealed by both parties. The full history is available via cello_transcript. No further actions are required on this session.",
|
|
5406
5419
|
};
|
|
5407
5420
|
}
|
|
5408
5421
|
// 3) Out of time — non-blocking-equivalent empty answer.
|
|
@@ -5420,13 +5433,12 @@ export async function startDaemon(config) {
|
|
|
5420
5433
|
guidance: "The counterparty's session connection has dropped (liveness: gone) — it may have crashed or gone offline. No more content will arrive on the direct path. Call cello_close_session to seal the session; if the counterparty never co-closes, a unilateral seal becomes available after the directory's delivery-grace window.",
|
|
5421
5434
|
};
|
|
5422
5435
|
}
|
|
5423
|
-
return { ok: true, content: null, guidance: "No content arrived within timeout_ms. Call cello_receive again to keep waiting, or read
|
|
5436
|
+
return { ok: true, content: null, guidance: "No content arrived within timeout_ms. Call cello_receive again to keep waiting, or read cello_transcript for the full session history." };
|
|
5424
5437
|
}
|
|
5425
5438
|
await new Promise((r) => setTimeout(r, Math.min(20, remaining)));
|
|
5426
5439
|
}
|
|
5427
5440
|
};
|
|
5428
5441
|
handlers.set("cello_receive", handleReceive);
|
|
5429
|
-
handlers.set("cello_receive_session", handleReceive);
|
|
5430
5442
|
// ─── M8C-INBOX-1 (N1/N4): cello_check_notifications — push-loss reconciler + poll-only inbox ───
|
|
5431
5443
|
// Notifications are fire-and-forget (no ack, no redelivery); this is how a client discovers what
|
|
5432
5444
|
// it missed while its shim was down/busy, and the primary inbox for poll-only clients (Bedrock,
|
|
@@ -5477,6 +5489,11 @@ export async function startDaemon(config) {
|
|
|
5477
5489
|
noticed_at: n.noticed_at,
|
|
5478
5490
|
// Names the contact by the operator's OWN pet name (AC3); the self-declared name is a quoted,
|
|
5479
5491
|
// untrusted claim; the adopt command carries its arguments so it is copy-pasteable.
|
|
5492
|
+
// The MCP call form below is translated WHOLE (arguments and all) for a CLI caller by
|
|
5493
|
+
// vocabulary.ts's CALL_FORMS — rewriting only the tool name would leave the tool's JSON
|
|
5494
|
+
// bolted onto a CLI verb, which looks like a command and is not one. `claimed_name` above
|
|
5495
|
+
// carries the peer's self-declared name UNTOUCHED, so the operator always has the raw value
|
|
5496
|
+
// even though the prose copy passes through the renderer.
|
|
5480
5497
|
notice: `Your contact ${n.moniker !== null ? `"${n.moniker}" ` : ""}(${n.pubkey.slice(0, 16)}…) now calls themselves ` +
|
|
5481
5498
|
`"${n.offered_name}" (self-declared — unverified). Adopt it: cello_contact_set_moniker ` +
|
|
5482
5499
|
`{ pubkey: "${n.pubkey}", moniker: "${n.offered_name}" }, or ignore.`,
|
|
@@ -5494,6 +5511,30 @@ export async function startDaemon(config) {
|
|
|
5494
5511
|
// M8C-CONTACT-1: cello contact add/remove/list [--agent <name>]. All three resolve the target
|
|
5495
5512
|
// agent the same way — an explicit params.agent, else this connection's current/sole-online
|
|
5496
5513
|
// agent (F18) — so a CLI/AI operator gets the same no_current_agent guidance INBOX already uses.
|
|
5514
|
+
/**
|
|
5515
|
+
* Review finding 7 — a contact pubkey must LOOK like a pubkey, or the row can never work.
|
|
5516
|
+
*
|
|
5517
|
+
* The address-book handlers took `params.pubkey` as any string and stored it. So
|
|
5518
|
+
* `cello contact tier add` — a plausible slip under the new `contact <pubkey> <op>` shape, where
|
|
5519
|
+
* the FIRST positional is the pubkey — persisted a contact whose pubkey is the literal text
|
|
5520
|
+
* "tier", and answered ok:true. Nothing would ever match it: it is not a key, so it can never be
|
|
5521
|
+
* a counterparty. The operator is told they added someone; they added a typo.
|
|
5522
|
+
*
|
|
5523
|
+
* A key is 32 bytes, hex: 64 characters. Anything else is refused, loudly, with the value echoed
|
|
5524
|
+
* so the slip is obvious. Returns null when the pubkey is absent — the caller's own
|
|
5525
|
+
* missing_params check owns that case and says something more useful.
|
|
5526
|
+
*/
|
|
5527
|
+
function invalidPubkey(pubkey) {
|
|
5528
|
+
if (pubkey === undefined)
|
|
5529
|
+
return null; // absent → the caller's missing_params check owns it
|
|
5530
|
+
if (/^[0-9a-fA-F]{64}$/.test(pubkey))
|
|
5531
|
+
return null;
|
|
5532
|
+
return {
|
|
5533
|
+
ok: false,
|
|
5534
|
+
reason: "invalid_pubkey",
|
|
5535
|
+
guidance: `'${pubkey}' is not a public key. A CELLO public key is 64 hex characters (32 bytes). Nothing was changed — storing it would create an address-book entry that can never match anyone. Note the argument order is 'cello contact <pubkey> <operation>'.`,
|
|
5536
|
+
};
|
|
5537
|
+
}
|
|
5497
5538
|
function resolveContactAgent(connState, params) {
|
|
5498
5539
|
const explicit = typeof params?.agent === "string" ? params.agent : undefined;
|
|
5499
5540
|
if (explicit)
|
|
@@ -5510,6 +5551,9 @@ export async function startDaemon(config) {
|
|
|
5510
5551
|
}
|
|
5511
5552
|
handlers.set("cello_contact_add", async (params, connectionId) => {
|
|
5512
5553
|
const pubkey = typeof params?.pubkey === "string" ? params.pubkey : undefined;
|
|
5554
|
+
const badPubkey = invalidPubkey(pubkey);
|
|
5555
|
+
if (badPubkey)
|
|
5556
|
+
return badPubkey;
|
|
5513
5557
|
if (!pubkey) {
|
|
5514
5558
|
return { ok: false, reason: "missing_params", guidance: "Provide 'pubkey' (hex) — the contact to add." };
|
|
5515
5559
|
}
|
|
@@ -5547,6 +5591,9 @@ export async function startDaemon(config) {
|
|
|
5547
5591
|
// key is NOT a clear (Entry-66-F3): a request that omits it is malformed and rejected.
|
|
5548
5592
|
handlers.set("cello_contact_set_moniker", async (params, connectionId) => {
|
|
5549
5593
|
const pubkey = typeof params?.pubkey === "string" ? params.pubkey : undefined;
|
|
5594
|
+
const badPubkey = invalidPubkey(pubkey);
|
|
5595
|
+
if (badPubkey)
|
|
5596
|
+
return badPubkey;
|
|
5550
5597
|
if (!pubkey || !params || !("moniker" in params)) {
|
|
5551
5598
|
return { ok: false, reason: "missing_params", guidance: "Provide 'pubkey' (hex) and 'moniker' — a string to set the pet name, or null to clear it." };
|
|
5552
5599
|
}
|
|
@@ -5572,6 +5619,9 @@ export async function startDaemon(config) {
|
|
|
5572
5619
|
// (0..4) — an unknown value is REFUSED, never coerced. Emits contact.tier.changed (old→new).
|
|
5573
5620
|
handlers.set("cello_contact_set_tier", async (params, connectionId) => {
|
|
5574
5621
|
const pubkey = typeof params?.pubkey === "string" ? params.pubkey : undefined;
|
|
5622
|
+
const badPubkey = invalidPubkey(pubkey);
|
|
5623
|
+
if (badPubkey)
|
|
5624
|
+
return badPubkey;
|
|
5575
5625
|
const tier = typeof params?.tier === "number" ? params.tier : undefined;
|
|
5576
5626
|
if (!pubkey || tier === undefined) {
|
|
5577
5627
|
return { ok: false, reason: "missing_params", guidance: "Provide 'pubkey' (hex) and 'tier' (0=blocked, 1=unknown, 2=known, 3=whitelisted, 4=vip)." };
|
|
@@ -5597,6 +5647,9 @@ export async function startDaemon(config) {
|
|
|
5597
5647
|
// on the outbound path at send time (SI), never here.
|
|
5598
5648
|
handlers.set("cello_contact_set_away", async (params, connectionId) => {
|
|
5599
5649
|
const pubkey = typeof params?.pubkey === "string" ? params.pubkey : undefined;
|
|
5650
|
+
const badPubkey = invalidPubkey(pubkey);
|
|
5651
|
+
if (badPubkey)
|
|
5652
|
+
return badPubkey;
|
|
5600
5653
|
if (!pubkey || !params || !("message" in params)) {
|
|
5601
5654
|
return { ok: false, reason: "missing_params", guidance: "Provide 'pubkey' (hex) and 'message' — a string away text, or null to clear it." };
|
|
5602
5655
|
}
|
|
@@ -5620,6 +5673,11 @@ export async function startDaemon(config) {
|
|
|
5620
5673
|
return { ok: true, agent: resolved.agent, pubkey };
|
|
5621
5674
|
});
|
|
5622
5675
|
handlers.set("cello_contact_remove", async (params, connectionId) => {
|
|
5676
|
+
// Deliberately NOT shape-gated. `remove` is the ESCAPE HATCH, and gating it would trap the very
|
|
5677
|
+
// rows the gate exists to prevent: a junk contact written by the older, unvalidated code (pubkey
|
|
5678
|
+
// = the literal text "tier") could never be deleted, because deleting it requires naming it.
|
|
5679
|
+
// Validation belongs on the paths that CREATE or MUTATE a row, never on the one that removes it.
|
|
5680
|
+
// Removing a key that was never stored is a harmless not_found.
|
|
5623
5681
|
const pubkey = typeof params?.pubkey === "string" ? params.pubkey : undefined;
|
|
5624
5682
|
if (!pubkey) {
|
|
5625
5683
|
return { ok: false, reason: "missing_params", guidance: "Provide 'pubkey' (hex) — the contact to remove." };
|
|
@@ -5714,7 +5772,7 @@ export async function startDaemon(config) {
|
|
|
5714
5772
|
}
|
|
5715
5773
|
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
5716
5774
|
if (!store.setMoniker(resolved.agent, moniker)) {
|
|
5717
|
-
return { ok: false, reason: "agent_not_found", guidance: `No active agent named '${resolved.agent}'. See
|
|
5775
|
+
return { ok: false, reason: "agent_not_found", guidance: `No active agent named '${resolved.agent}'. See cello_agents.` };
|
|
5718
5776
|
}
|
|
5719
5777
|
logger.info("agent.moniker.set", { agentName: resolved.agent, cleared: moniker === null });
|
|
5720
5778
|
return { ok: true, agent: resolved.agent, moniker };
|
|
@@ -5744,8 +5802,28 @@ export async function startDaemon(config) {
|
|
|
5744
5802
|
}
|
|
5745
5803
|
return { acknowledged: true };
|
|
5746
5804
|
});
|
|
5805
|
+
// DOD-ONBOARD-HELP-1 §5 — render every response for the surface that asked.
|
|
5806
|
+
//
|
|
5807
|
+
// The daemon is where an operator actually MEETS the tool names: a refused send says "call
|
|
5808
|
+
// cello_receive first". Those strings are written with the canonical MCP names, so an MCP caller
|
|
5809
|
+
// gets them verbatim — but a CLI caller must be told `cello receive`, the thing they can type.
|
|
5810
|
+
// (P2-7 was the one-off report of this; it is a whole CLASS, and this closes the class.)
|
|
5811
|
+
//
|
|
5812
|
+
// Wrapping the handler map is the ONE choke point that has both the response and the connection's
|
|
5813
|
+
// clientType. Doing it per-handler would mean 60+ call sites, and the 61st would forget.
|
|
5814
|
+
const renderedHandlers = new Map();
|
|
5815
|
+
for (const [method, handler] of handlers) {
|
|
5816
|
+
renderedHandlers.set(method, async (params, connectionId) => {
|
|
5817
|
+
const result = await handler(params, connectionId);
|
|
5818
|
+
// Default to "cli": a connection that never sent ipc.connect has no recorded surface, and the
|
|
5819
|
+
// CLI verb is the safe answer — it is at least a real command an operator can run, whereas an
|
|
5820
|
+
// MCP tool name is useless in a terminal.
|
|
5821
|
+
const surface = perConnectionState.get(connectionId)?.clientType === "mcp" ? "mcp" : "cli";
|
|
5822
|
+
return renderForSurface(result, surface);
|
|
5823
|
+
});
|
|
5824
|
+
}
|
|
5747
5825
|
// Create and start IPC server
|
|
5748
|
-
const ipcServer = createIpcServer({ socketPath, maxConnections, logger },
|
|
5826
|
+
const ipcServer = createIpcServer({ socketPath, maxConnections, logger }, renderedHandlers);
|
|
5749
5827
|
try {
|
|
5750
5828
|
await ipcServer.start();
|
|
5751
5829
|
}
|