omp-onlyne 0.1.0 → 0.3.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 CHANGED
@@ -24,6 +24,7 @@ key_scope = "project"
24
24
 
25
25
  [server]
26
26
  bind = "127.0.0.1"
27
+ # port is written by the runtime; set it only to pin one by hand.
27
28
 
28
29
  [threads]
29
30
  page_size = 50
@@ -35,12 +36,15 @@ references. `/onlyne init` suggests region from the current Tern session name
35
36
  and leaves the node id empty for the user. A short region is saved as
36
37
  `urn:onlyne:region:<encoded name>`.
37
38
 
38
- The listening port is allocated by the operating system at startup; the runtime
39
- derives the endpoint and publishes it through discovery. Discovery uses a seed
40
- address supplied during bootstrap and an HTTP candidate exchange stored in
41
- `.onlyne/omp.db`. DNS-SD is an optional same-link accelerator: install
39
+ The first start lets the operating system choose the listening port, and the
40
+ runtime writes that port back as `server.port`; every later start reuses it, so
41
+ the address a peer recorded keeps answering. A port another process already
42
+ holds moves the node to a system-chosen one, which is written back the same way.
43
+ The runtime derives the endpoint and publishes it through discovery. Discovery
44
+ uses a seed address supplied during bootstrap and an HTTP candidate exchange
45
+ stored in `.onlyne/omp.db`. DNS-SD is an optional same-link accelerator: install
42
46
  `bonjour-service` to enable it. The configuration does not contain endpoint,
43
- port, peers, task-spawn policy or supplement windows.
47
+ peers, task-spawn policy or supplement windows.
44
48
 
45
49
  `node.key_scope` names where the identity key lives. `project` keeps it in
46
50
  `<project>/.onlyne/keys`, which a git work tree must ignore; the node refuses to
@@ -66,6 +70,10 @@ separate security proposal; leave both at their defaults for the first E2E.
66
70
  `onlyne://outbound`, `onlyne://neighbors` — read-only views. List views accept
67
71
  `page_size` (or `limit`) and `cursor`; a conversation holds both directions of one
68
72
  exchange.
73
+ - `onlyne://threads` also lists the threads this node was invited into and does not own.
74
+ Those rows carry the owner, the last sequence the local records know, and the member
75
+ list when the owner answers; they are read-only, so the composer refuses them and
76
+ entries are read from the owner.
69
77
  - `onlyne://resources` reads the local node and peers whose key exchange is complete.
70
78
  Each owner returns its authorized resources or an explicit failure. Select
71
79
  `onlyne://resources/<encoded-owner>` for one owner's inventory and pagination.
@@ -84,9 +92,9 @@ separate security proposal; leave both at their defaults for the first E2E.
84
92
  the record.
85
93
  - `/onlyne` opens a domain rail, thread sidebar, message flow and domain node list.
86
94
  Threads, messages and nodes scroll independently. The inbox opens from the sidebar.
87
- Posting names the thread's owner and id; clicking an incoming message selects its
88
- message reference for a reply. Reply routing currently follows the original
89
- author and can land in another thread. `/onlyne settings` edits the project
95
+ Posting names the thread's owner and id; clicking a message opens its full body,
96
+ and Reply or Use as recipient are separate actions in that detail. Reply routing
97
+ follows the original author and can land in another thread. `/onlyne settings` edits the project
90
98
  configuration. `/onlyne init` writes a first config and starts the node; with
91
99
  no config, `/onlyne` and `/onlyne settings` open the same bootstrap.
92
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-onlyne",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Onlyne v3 for OMP: one msg tool, thread-aware attention and SystemOne delivery decisions",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -39,8 +39,8 @@
39
39
  "test": "bun test"
40
40
  },
41
41
  "dependencies": {
42
- "@onlyne/lib": "^3.0.0",
43
- "@onlyne/node-host": "^3.0.0"
42
+ "@onlyne/lib": "^3.2.0",
43
+ "@onlyne/node-host": "^3.2.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@oh-my-pi/pi-agent-core": "*",
@@ -22,10 +22,8 @@ const en = {
22
22
  draftNotFound: "No reply draft is named {id}.",
23
23
  draftNeedsReference: "A reply draft needs in_reply_to {sender,id} or {thread,seq}.",
24
24
  draftSourceUnavailable: "The draft source message is unavailable.",
25
-
26
- contextNotRegistered: "No execution context is registered as {alias}.",
27
- mainSummary: "Main OMP session",
28
- storeClosed: "The Onlyne store is closed.",
25
+ draftNoMatch: "No stored reply draft answers {sender}/{id}. Create the draft first, or pass meta.draft_id.",
26
+ draftNoRecent: "This session has no stored reply draft to send. Create the draft first, or pass meta.draft_id.",
29
27
 
30
28
  todoContextRequired: "A todo decision needs a non-empty context alias.",
31
29
  todoUnknownAction: "The judge returned an unknown todo action: {action}.",
@@ -51,6 +49,24 @@ const en = {
51
49
  todoNoLinks: "Onlyne todo metadata on entry {entry} has no links array; the association cannot be replayed.",
52
50
  todoUnknownRecordedAction: "Onlyne todo decision on entry {entry} has an unknown action.",
53
51
  todoMissingString: "Onlyne todo metadata on entry {entry} has no string {field}.",
52
+
53
+ trustHeader: "Onlyne trust · {phase} · {node}",
54
+ trustFingerprint: "fingerprint: {fingerprint}",
55
+ trustDeclared: "declared address: {declared}",
56
+ trustObserved: "observed address: {observed}",
57
+ trustUnobserved: "not observed",
58
+ trustPolicy: "acquisition policy: {policy}",
59
+ trustJudgement: "judgement: {reason} · {conclusion}",
60
+ trustAllowed: "allowed",
61
+ trustRefused: "refused",
62
+ trustInvited: "This peer's approval waits for yours. Decide it explicitly:",
63
+ trustApproved: "The acquisition policy approved this peer before any human confirmed, and the approval is committed. No action is requested.",
64
+ trustRequested: "Local approval is committed; the exchange completes when the peer confirms. This is a status update; no action is requested.",
65
+ trustExchanged: "Both sides approved. This is a status update; no action is requested.",
66
+ trustRevoked: "Local approval was dropped. This is a status update; no action is requested.",
67
+ trustAcceptAction: 'accept: msg({ delivery_kind: "accept the trust handshake", msg: "accept {node}", meta: { op: "trust_accept", node_id: "{node}", fingerprint: "{fingerprint}" } })',
68
+ trustRevokeAction: 'revoke: msg({ delivery_kind: "revoke the trust handshake", msg: "revoke {node}", meta: { op: "trust_revoke", node_id: "{node}" } })',
69
+ trustPanelOnly: "The acquisition policy is none, so this approval is the human's: only the panel can accept it. You may read and revoke.",
54
70
  };
55
71
 
56
72
  const zh: Record<keyof typeof en, string> = {
@@ -75,10 +91,8 @@ const zh: Record<keyof typeof en, string> = {
75
91
  draftNotFound: "没有名为 {id} 的回复草稿。",
76
92
  draftNeedsReference: "回复草稿需要 in_reply_to {sender,id} 或 {thread,seq}。",
77
93
  draftSourceUnavailable: "草稿的源消息不可用。",
78
-
79
- contextNotRegistered: "没有注册名为 {alias} 的执行上下文。",
80
- mainSummary: "OMP 主会话",
81
- storeClosed: "Onlyne 存储已关闭。",
94
+ draftNoMatch: "没有回答 {sender}/{id} 的已存回复草稿。请先起草,或传 meta.draft_id。",
95
+ draftNoRecent: "本会话没有可发送的已存回复草稿。请先起草,或传 meta.draft_id。",
82
96
 
83
97
  todoContextRequired: "todo 决策需要非空的上下文别名。",
84
98
  todoUnknownAction: "judge 返回了未知的 todo 动作:{action}。",
@@ -104,6 +118,24 @@ const zh: Record<keyof typeof en, string> = {
104
118
  todoNoLinks: "条目 {entry} 上的 onlyne todo 元数据没有 links 数组;无法重放该关联。",
105
119
  todoUnknownRecordedAction: "条目 {entry} 上的 onlyne todo 决策含有未知动作。",
106
120
  todoMissingString: "条目 {entry} 上的 onlyne todo 元数据缺少字符串字段 {field}。",
121
+
122
+ trustHeader: "Onlyne 信任 · {phase} · {node}",
123
+ trustFingerprint: "指纹:{fingerprint}",
124
+ trustDeclared: "声明地址:{declared}",
125
+ trustObserved: "观测地址:{observed}",
126
+ trustUnobserved: "未观测到",
127
+ trustPolicy: "获取策略:{policy}",
128
+ trustJudgement: "判定:{reason} · {conclusion}",
129
+ trustAllowed: "允许",
130
+ trustRefused: "拒绝",
131
+ trustInvited: "这个对端的批准在等你。请显式决定:",
132
+ trustApproved: "获取策略在任何人工确认之前批准了这个对端,批准已经提交。不索取任何动作。",
133
+ trustRequested: "本地批准已提交;对端确认后交换完成。这是状态更新,不索取任何动作。",
134
+ trustExchanged: "双方都已批准。这是状态更新,不索取任何动作。",
135
+ trustRevoked: "本地批准已撤销。这是状态更新,不索取任何动作。",
136
+ trustAcceptAction: '批准:msg({ delivery_kind: "批准信任握手", msg: "批准 {node}", meta: { op: "trust_accept", node_id: "{node}", fingerprint: "{fingerprint}" } })',
137
+ trustRevokeAction: '撤销:msg({ delivery_kind: "撤销信任握手", msg: "撤销 {node}", meta: { op: "trust_revoke", node_id: "{node}" } })',
138
+ trustPanelOnly: "获取策略是 none,这次批准属于人类:只有面板能接受它。你可以读取与撤销。",
107
139
  };
108
140
 
109
141
  export const bridgeText = createTranslator({ en, "zh-CN": zh });
package/src/config.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
- import type { OnlyneConfig } from "./types.ts";
3
+ import type { OnlyneConfig } from "@onlyne/node-host/types";
4
4
  import type { Locale } from "@onlyne/node-host/i18n";
5
- import { hostText as t } from "./host-text.ts";
5
+ import { hostText as t } from "@onlyne/node-host/host-text";
6
+ import { DEFAULT_ACQUISITION, parseAllowBlock, type AcquirePolicy, type AcquisitionPolicy } from "@onlyne/node-host/acquire";
6
7
 
7
8
  const CONFIG_RELATIVE_PATH = join(".onlyne", "config.toml");
8
9
  const REGION_PREFIX = "urn:onlyne:region:";
@@ -136,8 +137,12 @@ export function parseConfig(text: string, path: string): OnlyneConfig {
136
137
  const keyRef = optionalString(node, "key_ref", "node.key_ref");
137
138
  const keyScope = optionalString(node, "key_scope", "node.key_scope");
138
139
  const publicUrl = optionalString(server, "public_url", "server.public_url");
140
+ const port = optionalInteger(server, "port", "server.port");
139
141
  const seeds = discovery === undefined ? undefined : stringArray(discovery, "seeds", "discovery.seeds");
140
142
  const trust = trustSeeds(raw, path);
143
+ const acquisition = raw.acquisition === undefined ? undefined : table(raw, "acquisition", path);
144
+ const acquirePolicy = acquisition === undefined ? undefined : optionalString(acquisition, "policy", "acquisition.policy");
145
+ const acquireAllow = acquisition === undefined ? undefined : stringArray(acquisition, "allow", "acquisition.allow");
141
146
  const bind = optionalString(server, "bind", "server.bind");
142
147
  const pageSize = optionalInteger(threads, "page_size", "threads.page_size");
143
148
  return validateConfig({
@@ -145,8 +150,9 @@ export function parseConfig(text: string, path: string): OnlyneConfig {
145
150
  ...(raw.language === undefined ? {} : { language: raw.language as Locale }),
146
151
  region: region ?? "",
147
152
  node: { id: id ?? "", ...(keyRef === undefined ? {} : { key_ref: keyRef }), ...(keyScope === undefined ? {} : { key_scope: keyScope as "project" | "global" | "none" }) },
148
- server: { bind: bind ?? DEFAULT_BIND, ...(publicUrl === undefined ? {} : { public_url: publicUrl }) },
153
+ server: { bind: bind ?? DEFAULT_BIND, ...(port === undefined ? {} : { port }), ...(publicUrl === undefined ? {} : { public_url: publicUrl }) },
149
154
  threads: { page_size: pageSize ?? DEFAULT_PAGE_SIZE },
155
+ acquisition: { policy: acquirePolicyOf(acquirePolicy), allow: acquireAllow ?? [...DEFAULT_ACQUISITION.allow] },
150
156
  ...(seeds === undefined && trust === undefined ? {} : { discovery: { ...(seeds === undefined ? {} : { seeds }), ...(trust === undefined ? {} : { trust }) } }),
151
157
  ...(trust === undefined ? {} : { trust }),
152
158
  });
@@ -167,11 +173,17 @@ export function serializeConfig(config: OnlyneConfig): string {
167
173
  "",
168
174
  "[server]",
169
175
  `bind = ${tomlString(config.server.bind)}`,
176
+ ...(config.server.port === undefined ? [] : [`port = ${String(config.server.port)}`]),
170
177
  ...(config.server.public_url === undefined ? [] : [`public_url = ${tomlString(config.server.public_url)}`]),
171
178
  "",
172
179
  "[threads]",
173
180
  `page_size = ${String(config.threads.page_size)}`,
174
181
  );
182
+ const acquisition = config.acquisition ?? DEFAULT_ACQUISITION;
183
+ lines.push("", "[acquisition]", `policy = ${tomlString(acquisition.policy)}`);
184
+ // An empty set is a decision ("approve nobody"), not an absent one: omitting the line
185
+ // would reload it as the default loopback set.
186
+ lines.push(`allow = [${acquisition.allow.map(entry => tomlString(entry)).join(", ")}]`);
175
187
  if (config.discovery?.seeds !== undefined || config.trust !== undefined) {
176
188
  lines.push("", "[discovery]");
177
189
  const seeds = config.discovery?.seeds ?? [];
@@ -192,11 +204,15 @@ function validateConfig(config: OnlyneConfig): OnlyneConfig {
192
204
  const keyRef = config.node.key_ref?.trim();
193
205
  const bind = config.server.bind.trim();
194
206
  const publicUrl = config.server.public_url?.trim();
207
+ const port = config.server.port;
195
208
  const keyScope = config.node.key_scope;
196
209
  if (keyScope !== undefined && keyScope !== "project" && keyScope !== "global" && keyScope !== "none") {
197
210
  throw new ConfigError("invalid", t("keyScopeInvalid", { value: JSON.stringify(keyScope) }));
198
211
  }
199
212
  if (publicUrl !== undefined && publicUrl.length === 0) throw new ConfigError("invalid", t("publicUrlEmpty"));
213
+ if (port !== undefined && (!Number.isInteger(port) || port < 0 || port > 65535)) {
214
+ throw new ConfigError("invalid", t("portRange", { value: String(port) }));
215
+ }
200
216
  // A seed is contacted at every start, so it is stored in the one spelling the node uses.
201
217
  const seeds = [...new Set((config.discovery?.seeds ?? []).map(seed => seed.trim()).filter(seed => seed.length > 0).map(normalizeServiceUrl))];
202
218
  if (bind.length === 0) throw new ConfigError("invalid", t("bindEmpty"));
@@ -207,13 +223,25 @@ function validateConfig(config: OnlyneConfig): OnlyneConfig {
207
223
  if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) {
208
224
  throw new ConfigError("invalid", t("pageSizeRange", { value: String(pageSize) }));
209
225
  }
226
+ const policy = config.acquisition?.policy ?? DEFAULT_ACQUISITION.policy;
227
+ if (policy !== "all" && policy !== "segment" && policy !== "none") {
228
+ throw new ConfigError("invalid", t("acquirePolicy", { value: JSON.stringify(policy) }));
229
+ }
230
+ const acquisition: AcquisitionPolicy = {
231
+ policy,
232
+ allow: [...new Set((config.acquisition?.allow ?? DEFAULT_ACQUISITION.allow).map(entry => entry.trim()).filter(entry => entry.length > 0))],
233
+ };
234
+ for (const entry of acquisition.allow) {
235
+ if (parseAllowBlock(entry) === undefined) throw new ConfigError("invalid", t("acquireAllow", { value: JSON.stringify(entry) }));
236
+ }
210
237
  return {
211
238
  path: config.path,
212
239
  ...(config.language === undefined ? {} : { language: config.language }),
213
240
  region,
214
241
  node: { id, ...(keyRef === undefined || keyRef.length === 0 ? {} : { key_ref: keyRef }), ...(keyScope === undefined ? {} : { key_scope: keyScope }) },
215
- server: { bind, ...(publicUrl === undefined ? {} : { public_url: publicUrl }) },
242
+ server: { bind, ...(port === undefined || port === 0 ? {} : { port }), ...(publicUrl === undefined ? {} : { public_url: publicUrl }) },
216
243
  threads: { page_size: pageSize },
244
+ acquisition,
217
245
  ...(seeds.length === 0 ? {} : { discovery: { seeds } }),
218
246
  ...(config.trust === undefined ? {} : { trust: config.trust }),
219
247
  };
@@ -226,7 +254,6 @@ function rejectRetired(
226
254
  threads: Record<string, unknown>,
227
255
  ): void {
228
256
  rejectKey(node, "endpoint", t("retiredEndpoint"));
229
- rejectKey(server, "port", t("retiredPort"));
230
257
  rejectKey(raw, "peers", t("retiredPeers"));
231
258
  rejectKey(raw, "omp", t("retiredOmp"));
232
259
  rejectKey(threads, "default_attention", t("retiredAttention"));
@@ -236,6 +263,13 @@ function rejectKey(source: Record<string, unknown>, key: string, message: string
236
263
  if (source[key] !== undefined) throw new ConfigError("invalid", message);
237
264
  }
238
265
 
266
+ /** `acquisition.policy`: the documented default when absent; any other word is a configuration error. */
267
+ function acquirePolicyOf(value: string | undefined): AcquirePolicy {
268
+ if (value === undefined) return DEFAULT_ACQUISITION.policy;
269
+ if (value === "all" || value === "segment" || value === "none") return value;
270
+ throw new ConfigError("invalid", t("acquirePolicy", { value: JSON.stringify(value) }));
271
+ }
272
+
239
273
  function table(root: Record<string, unknown>, key: string, path: string): Record<string, unknown> {
240
274
  const value = root[key];
241
275
  if (value === undefined) return {};
@@ -7,11 +7,11 @@ import { createTranslator } from "@onlyne/node-host/i18n";
7
7
  const en = {
8
8
  "judge.o1.instructions":
9
9
  "Choose the one action the model's message asks for. communicate: deliver a message, reply or progress note to another agent. inspect: look up local or network state. manage_thread: create, close, describe or change members of a local thread. manage_domain: manage this node's domain membership. operate_resource: operate a resource the message names explicitly.",
10
- "judge.o1.communicate": "The message is meant for another agent or node, including asking a peer something.",
10
+ "judge.o1.communicate": "The message is meant for another agent or node: delivering, replying, asking, reporting, or starting a new task or topic with one. Work another agent should carry out is this intent even when the action sounds physical (deploy, archive, clean up).",
11
11
  "judge.o1.inspect":
12
- "The message asks to see node-level state: who is online, the inbox, a thread's messages, one message, or delivery evidence. The thread registry and a thread's members are not this.",
12
+ "The message asks to see node-level state: who is online, the neighbors or contacts list, the inbox, a thread's messages, one message, or delivery evidence. Listing or reading any of these is inspect. The thread registry and a thread's members are not this.",
13
13
  "judge.o1.manage_thread":
14
- "The message works on threads themselves: list the threads, describe one thread's members, create, close, add or remove a member. Reading a thread's messages is inspect.",
14
+ "The message works on threads themselves: list the threads, describe one thread's members, create, close, add or remove a member. Reading a thread's messages is inspect. Starting a new task or conversation with a peer is communicate, not this.",
15
15
  "judge.o1.manage_domain":
16
16
  "The message changes or reads this node's domain membership: join, leave, list or describe the membership, or discover a domain's members.",
17
17
  "judge.o1.operate_resource":
@@ -20,12 +20,14 @@ const en = {
20
20
  "judge.relation.instructions":
21
21
  "The message references an earlier message. Is it a final reply or a progress supplement?",
22
22
  "judge.relation.reply": "A final answer that may release the original sender's wait.",
23
- "judge.relation.followup": "A supplement or progress note that does not release a wait.",
23
+ "judge.relation.followup": "A supplement or progress note that does not release a wait. A courtesy acknowledgement of a message that asked for nothing (收到、感谢同步) is this, or a plain new message — not a reply that releases an expectation nobody holds.",
24
24
 
25
25
  "judge.target.instructions":
26
- "Choose the node that should receive this message. Each candidate is named by its node id; the alias is its short name.",
26
+ "Choose the node that should receive this message. Each candidate is named by its node id; the alias is its short name. none: no candidate is named or implied by this call — refusing beats a random guess.",
27
27
  "judge.target.peer":
28
28
  "Discovered peer {node} at {base_url} (instance {instance}, last seen {last_seen}).",
29
+ "judge.target.none":
30
+ "No candidate is named or implied by this call; sending would be a guess.",
29
31
 
30
32
  "judge.response.instructions":
31
33
  "Two facts decide: does the receiver have to act, and can the sender continue without the answer?",
@@ -50,7 +52,7 @@ const en = {
50
52
  "The message does not continue any published context and starts fresh work instead.",
51
53
 
52
54
  "judge.inbound.instructions":
53
- "Choose where this inbound message is handled. main_agent: the main session takes it. A context alias: the named execution context continues the work it was created for. new_subagent: fresh independent work in a new subagent.",
55
+ "Choose where this inbound message is handled. main_agent: the main session takes it — a message that needs no answer and is not a self-contained work item always lands here, however clearly it says no follow-up is wanted. A context alias: the named execution context continues the work it was created for. new_subagent: fresh independent work in a new subagent.",
54
56
  "judge.inbound.main_agent": "The message needs an answer, or no registered context's work matches it.",
55
57
  "judge.inbound.context": "{summary} (state: {state}, agent: {agent})",
56
58
  "judge.inbound.context_same_thread":
@@ -59,11 +61,11 @@ const en = {
59
61
  "The message is a self-contained work item with its own background, and needs no answer. A status note, greeting, relay or summary is not this.",
60
62
 
61
63
  "judge.needs_answer.instructions":
62
- "Does the sender expect this node to answer? A reference to an earlier message does not by itself create that expectation.",
64
+ "Does the sender expect this node to answer? A reference to an earlier message does not by itself create that expectation, and a message that explicitly says no reply is needed (不用回、不用发给我、报告直接入库即可) is always no, however much work it requests.",
63
65
  "judge.needs_answer.yes":
64
66
  "The sender asks a question, greets and waits for an answer, requests work whose result must be reported back, or otherwise needs a reply from this node. plan.response await or async is consistent with this.",
65
67
  "judge.needs_answer.no":
66
- "The sender expects nothing back: it completes a reply this node asked for, reports status, records a fact, or says no answer is needed. plan.response none is consistent with this.",
68
+ "The sender expects nothing back: it completes a reply this node asked for, reports status, records a fact, or explicitly says no reply is needed. plan.response none is consistent with this.",
67
69
 
68
70
  "judge.insertion.instructions":
69
71
  "The message goes to the main session. Choose when it should enter the agent's work.",
@@ -118,10 +120,17 @@ const en = {
118
120
  "judge.domain_op.describe": "Describe this node's membership.",
119
121
  "judge.domain_op.discover": "Discover a domain's declared members.",
120
122
 
123
+ "judge.original.instructions":
124
+ "This call states neither an in_reply_to reference nor a relation. Decide whether it answers one of the listed inbound messages — records this context can see that nobody has answered yet — or starts a new message. Decide from what the call is for; recency or being the only candidate never makes a call an answer.",
125
+ "judge.original.new": "Not an answer to any of them; a new message on its own topic.",
126
+ "judge.original.candidate": "Answer the inbound message from {from} ({kind}) at thread {thread} seq {seq}: {body}",
127
+
121
128
  "error.outbound_intent": "The decision did not choose an outbound intent.",
122
129
  "error.relation_needs_reference": "A relation needs an in_reply_to reference.",
123
130
  "error.reply_authority":
124
131
  "The reply must go to its original reply authority {authority}, not {target}.",
132
+ "error.reply_reference_conflict":
133
+ "The referenced message id belongs to {actual}, not to the sender you named ({named}). Answer that message, or name its sender.",
125
134
  "error.delivery_target": "The decision did not choose a delivery target.",
126
135
  "error.execution_context_missing":
127
136
  '{to} publishes no execution context this message continues. Send it with execution "new" to start one.',
@@ -136,7 +145,6 @@ const en = {
136
145
  "error.contexts_unreadable": "The target's published contexts cannot be read: {reason}",
137
146
  "error.context_not_published": "The target does not publish an execution context named {alias}.",
138
147
  "error.thread_reference_shape": "meta.thread must be a thread alias or {owner,id}.",
139
- "error.ambiguous_local_thread": "More than one local thread is named {value}.",
140
148
  "error.routing_target": "The decision did not choose a routing target.",
141
149
  "error.needs_answer": "The decision did not say whether this message needs an answer.",
142
150
  "error.registry_unreadable": "The context registry could not be read: {error}",
@@ -154,7 +162,6 @@ const en = {
154
162
  "error.thread_title_required": "thread create needs a title.",
155
163
  "error.thread_reference_required": "thread {op} needs a thread reference.",
156
164
  "error.thread_target_required": "thread {op} needs a target.",
157
- "error.resource_fields": "resource needs owner, id and operation.",
158
165
  "error.read_thread_required": "read {name} needs a thread reference.",
159
166
  "error.read_message_required":
160
167
  "read {name} needs a message reference: meta.message {sender,id} or {thread,seq}.",
@@ -171,7 +178,7 @@ const en = {
171
178
  "error.domain_op": "The judge did not choose a domain operation.",
172
179
  "error.domain_id": "domain {op} needs domain_id.",
173
180
  "error.resource_action_fields":
174
- "A resource action needs meta.resource {owner,id} and meta.operation.",
181
+ "A resource action needs meta.resource {owner,id} and meta.operation. If this call was meant as a message to a peer (including an instruction to change a file on its own node), send it with meta.to or meta.in_reply_to instead.",
175
182
  "error.meta_to_single":
176
183
  "meta.to names one target; pass an array to the tool for several, or send one call per peer.",
177
184
  "error.meta_to_empty": "meta.to must be a non-empty string.",
@@ -222,11 +229,11 @@ const en = {
222
229
  const zh: Record<keyof typeof en, string> = {
223
230
  "judge.o1.instructions":
224
231
  "选择模型这条消息要求执行的那一个动作。communicate:向另一个 agent 投递消息、回复或进度说明。inspect:查询本地或网络状态。manage_thread:创建、关闭、描述本地 thread 或变更其成员。manage_domain:管理本节点的域成员身份。operate_resource:操作消息明确点名的资源。",
225
- "judge.o1.communicate": "这条消息是发给另一个 agent 或节点的,包括向对端提问。",
232
+ "judge.o1.communicate": "这条消息是发给另一个 agent 或节点的:投递、回复、提问、汇报,或与对端开启一项新任务、新话题。交给另一个 agent 去做的操作也属此类,即使动作听起来像物理操作(部署、归档、清理)。",
226
233
  "judge.o1.inspect":
227
- "这条消息要求查看节点级状态:谁在线、inbox、某个 thread 的消息、单条消息或投递凭据。thread 注册表和某个 thread 的成员不属于此。",
234
+ "这条消息要求查看节点级状态:谁在线、邻居或联系人列表、inbox、某个 thread 的消息、单条消息或投递凭据。列出或读取其中任何一项都属 inspect。thread 注册表和某个 thread 的成员不属于此。",
228
235
  "judge.o1.manage_thread":
229
- "这条消息作用于 thread 本身:列出 thread、描述某个 thread 的成员、创建、关闭、添加或移除成员。读取 thread 内的消息属于 inspect。",
236
+ "这条消息作用于 thread 本身:列出 thread、描述某个 thread 的成员、创建、关闭、添加或移除成员。读取 thread 内的消息属于 inspect。与对端开启一项新任务或新对话属于 communicate,不属于此。",
230
237
  "judge.o1.manage_domain":
231
238
  "这条消息改变或读取本节点的域成员身份:加入、离开、列出或描述成员身份,或发现某个域的成员。",
232
239
  "judge.o1.operate_resource":
@@ -234,12 +241,19 @@ const zh: Record<keyof typeof en, string> = {
234
241
 
235
242
  "judge.relation.instructions": "这条消息引用了更早的一条消息。它是最终回复还是进度补充?",
236
243
  "judge.relation.reply": "一个最终答复,可以解除原发送方的等待。",
237
- "judge.relation.followup": "一个补充或进度说明,不解除任何等待。",
244
+ "judge.relation.followup": "一个补充或进度说明,不解除任何等待。对并未索取回答的消息致意确认(收到、感谢同步)属此,或干脆作为新消息——而不是解除无人持有的等待的 reply。",
245
+
246
+ "judge.original.instructions":
247
+ "这次调用既没有 in_reply_to 引用,也没有 relation。请判断它是否在回答下面列出的某条入站消息(本上下文可见、且尚无人答复的记录),还是一条独立的新消息。按调用的目的判断;最新或唯一候选本身不能说明它就是回答。",
248
+ "judge.original.new": "都不是;这是一条独立话题的新消息。",
249
+ "judge.original.candidate": "回答来自 {from} 的入站消息({kind}),位于 thread {thread} seq {seq}:{body}",
238
250
 
239
251
  "judge.target.instructions":
240
- "选择应当接收这条消息的节点。每个候选以其 node id 命名,alias 是它的短名。",
252
+ "选择应当接收这条消息的节点。每个候选以其 node id 命名,alias 是它的短名。none:这次调用没有点名或暗示任何候选——宁可拒绝也不随机猜测。",
241
253
  "judge.target.peer":
242
254
  "已发现的 peer {node},位于 {base_url}(实例 {instance},最后出现于 {last_seen})。",
255
+ "judge.target.none":
256
+ "这次调用没有点名或暗示任何候选;强行发送只是猜测。",
243
257
 
244
258
  "judge.response.instructions":
245
259
  "两个事实决定:接收方是否必须动手,以及发送方能否在没有答复的情况下继续。",
@@ -261,7 +275,7 @@ const zh: Record<keyof typeof en, string> = {
261
275
  "judge.context.none": "这条消息不延续任何已发布的上下文,而是开启新工作。",
262
276
 
263
277
  "judge.inbound.instructions":
264
- "选择这条入站消息在哪里处理。main_agent:由主会话处理。上下文别名:由该执行上下文延续它当初被创建来处理的工作。new_subagent:在新的 subagent 中处理独立的新工作。",
278
+ "选择这条入站消息在哪里处理。main_agent:由主会话处理——不需要答复、也不是自成一体工作项的消息一律落在这里,无论它把「不用后续处理」说得多明确。上下文别名:由该执行上下文延续它当初被创建来处理的工作。new_subagent:在新的 subagent 中处理独立的新工作。",
265
279
  "judge.inbound.main_agent": "这条消息需要答复,或没有任何已注册上下文的工作与之匹配。",
266
280
  "judge.inbound.context": "{summary}(state: {state}, agent: {agent})",
267
281
  "judge.inbound.context_same_thread":
@@ -270,7 +284,7 @@ const zh: Record<keyof typeof en, string> = {
270
284
  "这条消息是一个自带背景、自成一体的工作项,且无需答复。状态说明、问候、转达或汇总不属于此。",
271
285
 
272
286
  "judge.needs_answer.instructions":
273
- "发送方是否期待本节点答复?仅引用更早的消息本身并不构成这种期待。",
287
+ "发送方是否期待本节点答复?仅引用更早的消息本身并不构成这种期待;明说不必回复(不用回、不用发给我、报告直接入库即可)的消息,无论请求多少工作,一律为 no。",
274
288
  "judge.needs_answer.yes":
275
289
  "发送方提出问题、问候并等待答复、请求必须回报结果的工作,或以其他方式需要本节点回复。plan.response 为 await 或 async 与此一致。",
276
290
  "judge.needs_answer.no":
@@ -329,6 +343,8 @@ const zh: Record<keyof typeof en, string> = {
329
343
  "error.outbound_intent": "决策没有选择出站意图。",
330
344
  "error.relation_needs_reference": "relation 需要 in_reply_to 引用。",
331
345
  "error.reply_authority": "回复必须发往其原始回复权威 {authority},而不是 {target}。",
346
+ "error.reply_reference_conflict":
347
+ "引用的消息 id 属于 {actual},而不是你指名的发送方({named})。请回答那条消息,或改用它的发送方。",
332
348
  "error.delivery_target": "决策没有选择投递目标。",
333
349
  "error.execution_context_missing":
334
350
  '{to} 未发布这条消息所延续的执行上下文。要以 execution "new" 发送以新建一个。',
@@ -342,7 +358,6 @@ const zh: Record<keyof typeof en, string> = {
342
358
  "error.contexts_unreadable": "无法读取目标已发布的上下文:{reason}",
343
359
  "error.context_not_published": "目标未发布名为 {alias} 的执行上下文。",
344
360
  "error.thread_reference_shape": "meta.thread 必须是 thread 别名或 {owner,id}。",
345
- "error.ambiguous_local_thread": "有多个本地 thread 名为 {value}。",
346
361
  "error.routing_target": "决策没有选择路由目标。",
347
362
  "error.needs_answer": "决策没有说明这条消息是否需要答复。",
348
363
  "error.registry_unreadable": "无法读取上下文注册表:{error}",
@@ -356,7 +371,6 @@ const zh: Record<keyof typeof en, string> = {
356
371
  "error.thread_title_required": "thread create 需要一个 title。",
357
372
  "error.thread_reference_required": "thread {op} 需要一个 thread 引用。",
358
373
  "error.thread_target_required": "thread {op} 需要一个目标。",
359
- "error.resource_fields": "resource 需要 owner、id 和 operation。",
360
374
  "error.read_thread_required": "read {name} 需要一个 thread 引用。",
361
375
  "error.read_message_required":
362
376
  "read {name} 需要一条消息引用:meta.message {sender,id} 或 {thread,seq}。",
@@ -372,7 +386,8 @@ const zh: Record<keyof typeof en, string> = {
372
386
  "error.thread_op_target": "judge 选择了 thread {op},但没有目标。",
373
387
  "error.domain_op": "judge 没有选择域操作。",
374
388
  "error.domain_id": "domain {op} 需要 domain_id。",
375
- "error.resource_action_fields": "资源动作需要 meta.resource {owner,id} 和 meta.operation。",
389
+ "error.resource_action_fields":
390
+ "资源动作需要 meta.resource {owner,id} 和 meta.operation。如果这次调用本意是发消息给某个 peer(包括让对方改它自己节点上的文件),请改用 meta.to 或 meta.in_reply_to 发送。",
376
391
  "error.meta_to_single":
377
392
  "meta.to 只能点名一个目标;要发给多个,请向工具传数组,或每个 peer 发一次调用。",
378
393
  "error.meta_to_empty": "meta.to 必须是非空字符串。",