Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@ npm run build && MOSS_REAL_TERMINALS=1 npm run test:filter -- --filter tui-real-

## 文档

- [嵌入宿主的持久补充指令与会话消费回执](docs/sdk-durable-steering.md)

- [`AGENTS.md`](AGENTS.md) —— 架构、分层规则、子系统导航、工程约定(工作合同)
- [`docs/release-policy.md`](docs/release-policy.md) —— 一个版本 / tag 声称了什么,又没声称什么
- [`docs/capability-layer.md`](docs/capability-layer.md) —— MCP / device / skill 能力层
Expand Down
11 changes: 11 additions & 0 deletions docs/sdk-durable-steering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Durable steering in an embedded host

An accepted steer belongs to the session inbox until its canonical message has been adopted. An unconsumed entry remains pending when a run ends. A new Agent using the same session directory can deliver it on the next run. Delivery uses a stable inbox entry ID, not the message body; two intentionally identical instructions remain distinct deliveries.

The built-in JSONL store implements `appendMessageOnce(sessionKey, deliveryId, message)`. Its message and delivery receipt share one locked, fsynced append. A retry after append succeeds but inbox acknowledgement fails returns `false` without appending another message. Replacing or compacting history preserves receipts. A duplicate receipt does not refresh the caller's observed history version or authorize an outdated replacement.

The in-memory store provides receipt idempotence for its own lifetime. Hosts that initially use memory can implement optional `ensureDurableSession(sessionKey)`. Moss requests this transition only before adopting deferred entries. The host must transfer a coherent `loadDeliverySnapshot` containing both messages and delivery IDs into its durable store, under the same session lock as subsequent operations. `replaceMessages` accepts those IDs as an optional third argument. The durable store becomes the sole canonical history; an intentionally empty durable history is not a missing session and must not be filled from an older archive.

After a producer crashes before it can defer its pending entries, the host can explicitly recover entries for proven terminated run IDs. Admission includes server-generated producer provenance. A different Agent instance, an empty local active-run map, another hostname or an inaccessible PID is not proof of termination. Studio's host uses same-host `ESRCH`; live or unknown producers remain pending.

Custom stores remain source compatible: these methods are optional. An append-only custom store retains append-then-ack behavior and does **not** gain a crash-safe exactly-once guarantee. A host must fail closed when it cannot transfer delivery receipts coherently. Existing explicit inbox drain APIs still transfer ownership to their caller; they are not automatic durable recovery.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rdk-moss/agent",
"version": "0.27.0",
"version": "0.28.0",
"private": false,
"type": "module",
"license": "MIT",
Expand Down
7 changes: 5 additions & 2 deletions src/cli/doctor-board-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,21 @@ export async function probeGatewayReachability(input: {
const timeoutMs = input.timeoutMs ?? GATEWAY_PROBE_MS;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
let response: Response | undefined;
try {
const res = await fetchFn(target, {
response = await fetchFn(target, {
method: 'GET',
headers: { 'user-agent': 'moss-doctor/1.0' },
signal: controller.signal,
});
return { kind: 'ok', host, status: res.status };
return { kind: 'ok', host, status: response.status };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { kind: 'unreachable', host, error: msg };
} finally {
clearTimeout(timer);
controller.abort();
await response?.body?.cancel().catch(() => {});
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/cli/doctor-rdk-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,19 +149,22 @@ export async function probeVerifiedWhls(input: {
for (const item of list) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
const responses: Response[] = [];
try {
const res = await fetchFn(item.url, {
method: 'HEAD',
signal: controller.signal,
headers: { 'user-agent': 'moss-doctor/1.0' },
});
responses.push(res);
// Some CDNs reject HEAD; fall back to ranged GET.
if (res.status === 405 || res.status === 501) {
const getRes = await fetchFn(item.url, {
method: 'GET',
signal: controller.signal,
headers: { 'user-agent': 'moss-doctor/1.0', range: 'bytes=0-0' },
});
responses.push(getRes);
results.push({
board: item.board,
url: item.url,
Expand All @@ -175,6 +178,10 @@ export async function probeVerifiedWhls(input: {
results.push({ board: item.board, url: item.url, ok: false });
} finally {
clearTimeout(timer);
controller.abort();
for (const response of responses) {
await response.body?.cancel().catch(() => {});
}
}
}
return results;
Expand Down
7 changes: 5 additions & 2 deletions src/cli/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,16 +393,19 @@ async function probeKeylessSearchReachability(
): Promise<'ok' | 'http-error' | 'unreachable'> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
let response: Response | undefined;
try {
const res = await fetch('https://www.bing.com/search?q=moss+agent', {
response = await fetch('https://www.bing.com/search?q=moss+agent', {
headers: { 'user-agent': 'Mozilla/5.0 (compatible; moss-doctor/1.0)' },
signal: controller.signal,
});
return res.ok ? 'ok' : 'http-error';
return response.ok ? 'ok' : 'http-error';
} catch {
return 'unreachable';
} finally {
clearTimeout(timer);
controller.abort();
await response?.body?.cancel().catch(() => {});
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/core/agent/moss-agent-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ export interface MossAgentConfig

workspaceDir?: string;

/** Owner-scoped inbox directory; defaults to workspaceDir's runtime inbox. */
sessionInboxDir?: string;

/** Shell-write confinement roots for exec tools (v0.9 W1). Undefined = unconstrained. */
execWriteRoots?: string[];

Expand Down
Loading
Loading