npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tansr/serve

v0.12.0

Published

tansr Agent 会话引擎 (tansr Agent Session Engine): the bare /v2 agent-session protocol engine (REST + SSE) you embed in your own Node service. Session auth is fully yours via the authenticate seam. This package is not a gateway — auth policy, rate limiting, q

Downloads

1,820

Readme

tansr Agent 会话引擎(@tansr/serve)

对外发布名 = @tansr/serve(用户拍板 2026-09-01)。workspace 内恒以 @tansr/server 引用;发布 tarball 由 pnpm release:pack-serve 盖发布名。 license = MIT(同批拍板;正式协议考察待全部开发完成后进行)。

定名(doc/118 §八 G1,2026-09-03 用户授权代拍):本包是 tansr Agent 会话引擎 ——多租 Agent 会话的协议引擎(/v2 REST + SSE)+ 内置真平台装配(A 案,用户 拍板 2026-09-01),以 npm 包形态嵌进你自己的 Node 服务本包不是网关: 「网关」一词在 tansr 体系内只指 tansr-api,鉴权策略/限流/配额/计费归开发者的登录态 体系与平台侧;引擎自带的准入帽、rateLimit 缝与上游治理层是自保护面,不是替你 做网关。wire 契约权威 = doc/98(Agent 会话服务 v2 契约冻结件)。

引擎负责传输与协议机器:11 个 /v2 端点路由、SSE 编帧与逐会话事件环形缓冲、 Last-Event-ID 重放、三类桥回执受理(工具/权限/提问)、多会话治理(保留窗/ 闲置回收/并发帽)、轮末出站 webhook 通知。会话鉴权完全归你:引擎只暴露 authenticate 注入缝,token 形制/签发/校验由你的登录态体系全权自定 (最大放权)——引擎恒不规定、恒不内置任何 token 服务。

真装配内置:createAgentSessionFactory({ platform }) 四行接通 tansr 平台——appid/appkey → per-endUser app_user 令牌(POST /v1/app-tokens, TWP 签名)→ bundle registry 模型装配 → 内核查询环真调模型,计费按 endUser 归因;不用自写 Agent、不依赖任何 tansr 私有仓。

安装

npm install @tansr/serve

运行时要求:Node ≥ 22.19。唯一第三方依赖 zod 随包解析;tansr 内部包 (@tansr/protocol@tansr/i18n@tansr/kernel@tansr/providers@tansr/sdk)已编译内联,恒不外泄安装面。

系统媒体工具 / System media tools

图像生成、视频生成、语音转文字和文字转语音均由内核系统工具执行,工具名为 ImageGenVideoGenSpeechToTextTextToSpeech。平台内置会话工厂根据应用能力 装配它们,并使用 SDK 的平台提供方调用授权模型;capabilities.platform 表示平台服务 授权,不表示独立的“平台工具”类别。平台令牌、模型权限、配额、计费及运行时权限继续生效。

Android/iOS 经 /v2 会话连接时,模型与系统媒体工具运行在开发者的 serve 进程中, 手机负责提交输入和展示真实工具事件/媒体产物,不因此取得服务端文件或终端命令权限。 用户主动录音转写、朗读使用既有音频直连端点;它们与模型调用媒体工具并存,不自动进入聊天历史。 SDK 进程内集成仍须通过 tools.builtin 显式选择四媒体;这与 serve 平台工厂按应用授权装配的入口不同。

Image generation, video generation, transcription, and speech synthesis are built-in system tools. The platform session factory assembles the authorized tools with platform-backed providers. capabilities.platform retains its service-authorization meaning. On Android and iOS, these tools run in the serve process; the mobile client renders their events and artifacts. User-triggered audio operations use the separate direct endpoints and do not fabricate model tool events or chat history.

会话元数据与开发者登录

新构建的 GET /v2/sessions/:id 可返回可选的 meta.contextmeta.media。 前者区分下一轮/执行轮模型、真实观测用量及本地上下文预算;后者列出四类媒体的应用授权、 模型与限制、采样时间。字段缺席表示未知,客户端不能拿累计 token 用量当作当前上下文占用。 媒体快照是会话装配时的授权目录,实际工具是否装配及执行权限仍以服务端执行为准。 这次没有增加 HTTP 切模端点,也没有修改已冻结的内核事件协议。

countSpeechCharactersplanSpeechInput 和相关类型从本包公开导出,用于按授权模型的 maxChars 校验或显式分段朗读;示例遇到尚无新出口的已发布包时会提示升级。 开发者登录服务与平台 app_user 分离:客户端向开发者的 login/refresh 入口取短效访问票, serve 的 authenticate 验证该票并返回 endUserId,平台 appkey 始终只留在服务端。 完整远程示例见 examples/serve-demo;有界续期不会自动重放音频或聊天 POST。

New builds may include optional meta.context and meta.media in session metadata. Missing fields mean unknown. Context estimates and observed billing usage are separate; the media catalog reflects the session's assembly-time authorization. Developer login and refresh credentials remain separate from platform app_user tokens. The public speech planner uses the authorized model's limit and explicit segmentation; authentication recovery never automatically replays paid POST requests.

集成骨架

import { createAgentSessionFactory, registerBuiltinLocales, startServer } from '@tansr/serve';
import type { AgentSessionsOptions } from '@tansr/serve';

registerBuiltinLocales(); // 可选:错误体文案本地化(不注册则回落键名)

// ② 真装配(内置,A 案):appid/appkey → per-endUser 令牌 → 内核查询环。
//    也可换成你自实现的 AgentSessionFactory(注入形,契约同形)。
const build = createAgentSessionFactory({
  platform: {
    apiBaseUrl: 'https://api.tansr.com',
    appId: process.env.TANSR_APP_KEY_ID!,  // 平台控制台签发
    appKey: process.env.TANSR_APP_KEY!,    // 恒不下发端、恒不入日志
  },
  cwd: process.cwd(),
});

const v2: AgentSessionsOptions = {
  // ① 鉴权缝(唯一鉴权面):你的登录态 → endUserId;null = 401。
  //    token 形制自定——自家 JWT/session cookie/OAuth/API key 皆可。
  authenticate: async (req) => {
    const user = await myAuth.verify(req.headers['authorization']);
    return user ? { endUserId: user.id } : null;
  },
  createSession: build.factory,
  // ③ 可选:SessionStore 读面(resume/列表)、治理参数、轮末 webhook 缝
  // store, governance, onTurnEndNotify, ...
};

const server = await startServer({
  port: 8787,
  host: '127.0.0.1',
  token: myV1Token, // /v1 面 Bearer(与 /v2 鉴权互不相通)
  createSession: myV1Factory,
  version: '1.0.0',
  v2,
});

平台内置形工具面按应用 bundle 有效能力档按位装配(doc/112:TodoWrite / AskUser / WebSearch / ImageGen / VideoGen 逐位随控制台「能力面」开关;逐会话远程 工具三桥受 customTools 位——位关而申报 clientTools 即 400 明告);应用平台类型 非「移动应用」时经 onPlatformWarningserver.platform.warning 帧提示 app_platform_mismatch(不拒)。profile 请求位不支持(带上会 400 明告)。 一体式可跑示例见仓内 examples/serve-demo/agent-real.ts

三层鉴权定盘(架构拍板 2026-08-31)

| 层 | 凭据 | 归属 | |---|---|---| | sdk ↔ serve(会话面) | 你自定的登录态/token | 完全开发者侧;引擎只经 authenticate 缝取 endUserId | | serve ↔ api(引擎上游) | appid + appkey(平台「应用」颁发) | serve 持有;以 appkey 换 per-endUser app_user 令牌供内部 harness 调模型,endUser 计费归因 | | sdk 持 appid | 公开应用标识(client_id 语义) | 可下发端亮明身份;恒非凭据 |

红线:appkey 恒不下发端;app_user 令牌恒不出 serve;/v2 恒不受理 /v1 Bearer。会话 id 恒非鉴权因子(逐端点归属校验独立成立,跨租 403/404)。

推送唤醒 webhook 缝(轮末出站通知;doc/98 §五-5.4,A4-2/D5)

移动端 App 退后台、SSE 断开后,轮跑完了怎么唤醒用户?引擎恒不内置推送 通道(FCM/厂商通道由你自接,D5 终拍:SDK 与 serve 恒不内置),只留一条 webhook 缝:AgentSessionsOptions.onTurnEndNotify 在场时,会话轮终局 (turn.completed / turn.aborted)且该刻无活跃 SSE 订阅者才向你的 URL POST 一份最小载荷(有订阅者 = 客户端在线,恒不发;同轮恒一发,防抖)。

const v2: AgentSessionsOptions = {
  // ...authenticate / createSession...
  onTurnEndNotify: {
    url: 'https://your-service.example/tansr/turn-end',
    secret: process.env.NOTIFY_SECRET, // 可选:HMAC-SHA256 对原始体全文签名
  },
};
  • 载荷(TurnEndNotifyPayload 冻结形,恒不携消息内容恒不携凭据): { sessionId, endUserId, turnId?, status: 'completed'|'aborted', reason?, lastSeq, ts } ——lastSeq 供客户端持水位比对判断是否需要重放追赶,ts 供弃过期/防重放; reason?(contract-v0.21 / RFC-SC-1)= 终局帧 TerminalReason 原值,单帧区分 client_gone(订阅者离场被策略止损)/ internal_error(内核故障)/ 用户中断 aborted_* 等,接收方按「已知值专项 + 未知值兜底」消费;
  • 签名:secret 在场时携 x-tansr-signature: v2=<hex>, v1=<hex> (TURN_END_NOTIFY_SIGNATURE_HEADER;FX-C-43 双签一版,signatureVersions 缺省 ['v2','v1']):v2 = protocol WEBHOOK_SIGNATURE_V2 canonical(随行 x-tansr-timestamp / x-tansr-nonce),v1 = 对原始请求体全文 HMAC-SHA256—— 接收端先验签再消费(tansrd 缺省只收 v2);
  • CLI 形态两键(FX-C-09/续):tansr serve --v2TANSR_SERVE_NOTIFY_URL (绝对 http(s) URL)/ TANSR_SERVE_NOTIFY_SECRET 两 env 接同一缝(见下文旋钮表 notify 组)——无 URL = 不出站;secret 单独在场忽略并告警;启动期 stdout 一行 只打 URL 的 origin 与「签名在场 / 缺席」,secret 与路径 / 查询串恒不打印;未开 --v2 时两键列入「仅对 /v2 宿主生效」告警;
  • 投递纪律:fire-and-forget 恒不阻断会话主链;2xx 即成功,否则指数退避 重试(缺省 1+2 次,V2_LIMITS.notifyMaxRetries/notifyTimeoutMs/notifyBackoffMs 可覆写);终败走 onDeliveryFailure 结构化通报;服务器优雅关闭期恒不出站;
  • 收到通知后:你的服务自行走 FCM/厂商推送叫醒 App;App 回连 GET /v2/sessions/:id/eventsLast-Event-ID 断点续传,窗口外走 history/resume 恢复链。

主要出口

  • startServer(options) / StartServerOptions(v2?: AgentSessionsOptions; v2 缺席 = /v2 面零暴露;v1?: V1SessionsOptions = /v1 终结记录两级逐出 governance.{retentionMs, maxRetainedSessions, sweepIntervalMs},缺席 = 不逐出 ——库形态终结会话永驻可重放,长驻宿主建议 30 min / 10 000,CLI tansr serve 即此缺省);
  • AgentSessionsOptions:authenticate(唯一鉴权缝)、createSession (AgentSessionFactory)、store(AgentStoreReader)、governancemediaMaxBodyBytesonTurnEndNotify;
  • 真装配面(A 案,2026-09-01):createAgentSessionFactory (platform 内置形 / factory 注入形)、createPlatformServeSessionFactorycreateAppTokenMinter / createPlatformSessionAssembler(铸令牌与 per-endUser 装配缝)、createServeAgentSessionStore(轮粒度持久 store)、 ServeSessionDriver / AgentSessionBridge / buildRemoteTool 及契约类型族 (ServeSessionFactory / ServeSessionExtras / AssembledSessionModel 等);
  • 契约持份:V2_LIMITS / V2_ERROR_CODE / CONTROL_FRAME / AGENT_SESSION_CONTRACT_VERSION / 请求体 zod schema 族 / 控制帧载荷类型;
  • SSE 机器:encodeSseFrame / encodeAgentStreamFrame / EventRingBuffer;
  • 事件日志接口缝(SC-35):SessionEventLog / SessionEventLogRead / SessionEventLogFactory / MemorySessionEventLog(缺省实现),经 AgentSessionsOptions.eventLog 注入(见 Operations「扩展档位」);
  • 轮末通知:TurnEndNotifier / TURN_END_NOTIFY_SIGNATURE_HEADER;
  • i18n 便携面:registerBuiltinLocales / setLocale / negotiateLocale

发布形态

  • 双态 exports:开发态 src/index.ts(workspace tsx 直消费);发布态 publishConfig 三键覆盖指 dist(pnpm pack 应用);
  • build.mjs(S-D1 内联范式):esbuild 内联 @tansr/protocol/@tansr/i18n/ @tansr/kernel/@tansr/providers/@tansr/sdkdist/index.js (含 createRequire 垫片治内联 CJS 依赖);tsc + rollup-plugin-dts 卷积 → dist/index.d.ts;产物守卫拒任何 @tansr/* 残留;
  • pnpm release:pack-serve:pack 窗口盖发布名 @tansr/serve + 剔内部 devDeps,打包后守卫复检 manifest/依赖面/文件表/license(MIT + LICENSE 件 版权行 Tansr)+ ⑥真装配出口断言(动态 import 验三出口可调 + wire/d.ts 锚);发布动作恒候用户口令。

宿主信号接线(Graceful shutdown)

@tansr/serve 以 npm 包嵌进你的 Node 进程,进程信号归宿主:引擎恒不在 process 上注册信号监听器(嵌入库不替宿主决定进程何时退出),也就不会替你 处理 SIGTERM/SIGINT——宿主必须自己接线。不接线的后果:编排器常规停机 (docker stop / systemd / Kubernetes 缺省都发 SIGTERM)= 进程被硬杀,全部 SSE 连接 reset、在飞轮丢、无收口日志。

startServer 返回的 server.drain({ timeoutMs })(幂等;缺省 30 s)按序做四件事: ① 拒新——readiness 翻红(server.readiness() 供你的 /readyz 读)、新建/resume 一律 503 drainingRetry-After、全部在场 SSE 下发一帧 retry: 长间隔 (sse.drainRetryMs,缺省 10 s,客户端换台后再连);② 等在飞轮——/v2 运行中的 轮至多等 timeoutMs 到终态,到点剩余 interrupt()(轮末 store 提交照走,resume 可 续);③ flush 落盘——等全部 fire-and-forget 的 store 提交/建行 settle (createAgentSessionFactoryflush);④ 停机——终结会话(有界:在飞 pump 至多等 closeAllTimeoutMs,缺省 5 s、Infinity 无界;到点记 server.close_timeout{face,pending,timeoutMs} 后照常继续,永不 settle 的 pump 不再能挂住停机;RF-03)、停监听、掐空闲连接、 有界等待在飞非 SSE 请求答完(≤ 5 s)再掐残余连接。回执 { completedTurns, abortedTurns, flushedCommits, durationMs } 可入日志。 server.close() 保持硬收口语义(立即中止在飞轮、不等落盘;drain 进行中调用则等 drain 完成);/v1 会话无粗态读面,drain 不等其在飞轮。

最小接线——两枚信号共用一个 handler,幂等位防重复进入;第二枚信号不要吞掉,应立即硬退 (运维按两次 Ctrl+C / 两次 SIGTERM 强停的习惯,吞掉 = 只能等编排器 SIGKILL):

let stopping = false;
const shutdown = (signal: NodeJS.Signals): void => {
  if (stopping) {
    console.error(`[my-service] ${signal} received again, exiting now`);
    process.exit(2);
  }
  stopping = true;
  console.error(`[my-service] ${signal} received, draining...`);
  server
    .drain({ timeoutMs: 30_000 })
    .then((report) => {
      console.error(`[my-service] drained`, report);
      process.exit(0);
    })
    .catch((error: unknown) => {
      console.error('[my-service] shutdown failed', error);
      process.exit(1);
    });
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

要点:

  • 宽限期 ≥ drain 超时 + 收尾余量:drain({ timeoutMs: 30_000 }) 最坏等 30 s 在飞轮
    • ≤ 5 s 被中止轮收口 + ≤ 5 s 连接收口;容器 stop_grace_period(compose 缺省 10 s)、Kubernetes terminationGracePeriodSeconds、systemd TimeoutStopSec 部署件统一 45 s(doc/118 §八 G10;deploy/serve-v2/deploy/serve/ 已写),或把 timeoutMs 调小并同步调小宽限。宽限到点编排器 SIGKILL,等价硬杀;
  • 信号要能到达 node:容器 CMD/ENTRYPOINT 用 exec 形(JSON 数组)或 --init(tini 作 PID 1 转发),不要 sh -c "node …"(sh 当 PID 1 不转发);
  • 退出码由你定:引擎恒不调 process.exit;示例里 drain() 成功 exit(0)、 失败 exit(1),与你的进程监督器约定一致即可;tansr serve(CLI 形态)订阅 SIGINT / SIGTERM / SIGHUP / SIGBREAK,首枚同走 drain(超时经 TANSR_SERVE_DRAIN_TIMEOUT_MS 可配,缺省 30 s),第二枚立即 exit(2),整链 drain 超时 + 15 s 未收口亦 exit(2), 两者都记 process.hard_exit{signal,reason,budgetMs,elapsedMs,exitCode}(RF-03;共享组件 packages/cli/src/assembly/process-lifecycle.ts);CLI 形态另把每轮墙钟缺省定为 maxTurnWallClockMs = 1800000(30 min;TANSR_SERVE_TURN_WALL_CLOCK_MS=0 停用;审查 #9)—— 库缺省仍不设,嵌入宿主自行决定;
  • Windows 注记:process.on('SIGTERM') 可注册但系统不会发出、taskkill /F 不发任何信号; 控制台关闭以 SIGHUP 到达且系统只留约 10 s(CLI 形态对 SIGHUP 收口预算恒 ≤ 3 s);SIGBREAK (Ctrl+Break)与 SIGINT 同义。本机开发以 Ctrl+C(SIGINT)收束,Linux/容器上 SIGTERM / SIGHUP 都会到;
  • onTurnEndNotify 的关系:优雅关闭期出站通知恒不发(见上文投递纪律), 客户端靠回连 Last-Event-ID / history / resume 追赶。
  • 进程级兜底(十王修案 FX-C-02):引擎恒不替你挂 unhandledRejection / uncaughtException 监听,但导出 installProcessGuards({ logger, onFatal, drain?, drainTimeoutMs? })(返回卸载函数) 供宿主入口安装一次:任何逃逸到进程级的拒绝 / 异常恒记一条结构化事件 process.unhandled_rejection / process.uncaught_exception{ origin, message, stack(≤ 2 KB), policy } (旧形 logger 回落 i18n 文案行);onFatal:'drain-exit' = 记完 → drain()(有界,缺省 10 s)→ exit(1)(多租常驻进程缺省:tansr serve / serve-v2 参考宿主 / tansrd);'log' = 记完继续 (单用户入口缺省:acp / mcp serve / headless / serve-demo)。env TANSR_SERVE_ON_UNHANDLED=log|drain-exitresolveServeRuntimeOptions(env).onUnhandled 翻转;路由层错误边界(SC-01)与建会临界区 (FX-C-01)是第一道,这是最后一道——不是替代品。

完整可跑示例见仓内 examples/serve-demo/server.ts(接线封装在 examples/serve-demo/runtime.ts)。

Operations(运维面:指标 / 健康 / 结构化日志 / request-id / 旋钮 / 部署)

以下全部属运维面,不属 doc/98 wire 契约(端侧不感知;三观测端点是否入契约候 G4-g 拍板)。数字口径与 SLO 词表见 doc/report/serve并发审计-D路-可观测性与横向扩展.md §七-1。

运维旋钮:TANSR_SERVE_* 环境变量 → 库选项(SC-32)

引擎的全部运维位都是 startServer / 工厂 / governor 的库选项;进程形态(容器、CLI tansr serve)需要一个配置入口,本包给出单一事实源 resolveServeRuntimeOptions(env) (纯函数,不读 process.env、不写日志;表 SERVE_RUNTIME_ENV 与名单 SERVE_RUNTIME_ENV_NAMES 同出口)。宿主拿到结构化结果后各自展开:

import { createUpstreamGovernor, resolveServeRuntimeOptions, startServer } from '@tansr/serve';

const runtime = resolveServeRuntimeOptions(process.env);
for (const w of runtime.warnings) logger.error(`ignoring ${w.name}="${w.value}": expected ${w.expected}`); // 非法值:忽略,恒不拒启
const governor = createUpstreamGovernor(runtime.upstreamGovernor);          // TANSR_SERVE_UPSTREAM_MAX_INFLIGHT → bulkhead
const build = createAgentSessionFactory({ cwd, logger, platform: { apiBaseUrl, appId, appKey, governor, ...runtime.session } }); // 分层超时 / 每轮墙钟
const server = await startServer({
  host, port, token, createSession, logger: runtime.logFormat === 'json' ? jsonLogger : textLogger, // TANSR_SERVE_LOG_FORMAT
  governor,
  ...runtime.server,                                                        // admission / http / sse / observability / eventBufferMaxBytes
  v2: { authenticate, createSession: build.factory, store: build.storeReader,
        ...runtime.v2 },                                                    // /v2 治理五键 + 分片码 sessionIdShard(TANSR_SERVE_SESSION_SHARD)
});

纪律(doc/118 §八 G5/G9 代拍):任一变量缺席/空串即不落键 → 未设 = 库缺省,行为零漂移; 非法值(非整数、越界、枚举外)进 warnings 由宿主一行告警后忽略,恒不拒启;applied 列出生效 的变量名,appliedRuntimeEnvByGroup(runtime, 'governance', 'v2') 可判某些组是否在场(CLI tansr serve 缺省是 /v1 面,治理五键与分片码在场即提示无效;tansr serve --v2 / TANSR_SERVE_V2=1 起挂 /v2 单运维方形 ——同一 Bearer token + x-tansr-end-user 头分域、store 落 <sessionsDir>/agent-v2/cwdPolicy = 启动 cwd 子树, 此时这些键有消费点;FX-C-46,doc/98 §十 2026-09-08 行)。建议值与部署口径见 deploy/serve-v2/README.md §三 与 deploy/serve-v2/.env.example;全表由 runtime-options.tsSERVE_RUNTIME_ENV 表生成如下(FX-C-41, doc/130 R-12 生成律:pnpm env:example 再生、pnpm docs:check 幂等门;同一张表还生成 packages/server/.env.example,pnpm env:check 锁住;process 组的 TANSR_SERVE_ON_UNHANDLEDinstallProcessGuards({ onFatal }),见上节「进程级兜底」;factory 组三键 → createAgentSessionFactory({ ...runtime.factory }) 顶层键,语义见表后一段):

全表 31 键(键序 = 表声明序;缺席 / 空串 = 库缺省,非法值忽略并告警):

| 变量 | 组 | 目标选项 | 形态 | 约束 | 单位 | |---|---|---|---|---|---| | TANSR_SERVE_MAX_ACTIVE_SESSIONS | admission | admission.maxActiveSessions | int | integer >= 1 | — | | TANSR_SERVE_MAX_SSE_CONNECTIONS | admission | admission.maxSseConnections | int | integer >= 1 | — | | TANSR_SERVE_MAX_INFLIGHT_BODY_BYTES | admission | admission.maxInflightBodyBytes | int | integer >= 1 | bytes | | TANSR_SERVE_ELD_THRESHOLD_MS | admission | admission.eventLoopDelayP99Ms | int | integer >= 1 | ms | | TANSR_SERVE_REQUEST_TIMEOUT_MS | http | http.requestTimeout | int | integer >= 0 | ms | | TANSR_SERVE_EXPOSE_OBSERVABILITY | observability | observability.expose | bool | 1|true|yes|on / 0|false|no|off | — | | TANSR_SERVE_EXPOSE_METRICS | observability | observability.exposeMetrics | bool | 1|true|yes|on / 0|false|no|off | — | | TANSR_SERVE_EVENT_BUFFER_MAX_BYTES | ring | eventBufferMaxBytes | int | integer >= 1 | bytes | | TANSR_SERVE_ORPHAN_GRACE_MS | governance | v2.governance.orphanGraceMs | int | integer >= 0 | ms | | TANSR_SERVE_IDLE_AFTER_GONE_MS | governance | v2.governance.idleAfterGoneMs | int | integer >= 0 | ms | | TANSR_SERVE_MAX_RETAINED_SESSIONS | governance | v2.governance.maxRetainedSessions | int | integer >= 0 | — | | TANSR_SERVE_MAX_SESSION_LIFETIME_MS | governance | v2.governance.maxSessionLifetimeMs | int | integer >= 1 | ms | | TANSR_SERVE_MAX_TURNS_PER_SESSION | governance | v2.governance.maxTurnsPerSession | int | integer >= 1 | — | | TANSR_SERVE_SESSION_SHARD | v2 | v2.sessionIdShard | int | integer in [0, 255] | — | | TANSR_SERVE_NOTIFY_URL | notify | v2.onTurnEndNotify.url | string | absolute http(s) URL | — | | TANSR_SERVE_NOTIFY_SECRET | notify | v2.onTurnEndNotify.secret | secret | non-empty string; value never logged; requires TANSR_SERVE_NOTIFY_URL | — | | TANSR_SERVE_UPSTREAM_MAX_INFLIGHT | upstream | governor.bulkhead.maxConcurrent | int | integer >= 1 | — | | TANSR_SERVE_UPSTREAM_CONNECT_TIMEOUT_MS | session | session.timeouts.connectTimeoutMs | int | integer >= 1 | ms | | TANSR_SERVE_UPSTREAM_IDLE_TIMEOUT_MS | session | session.timeouts.idleTimeoutMs | int | integer >= 1 | ms | | TANSR_SERVE_UPSTREAM_TOTAL_TIMEOUT_MS | session | session.timeouts.totalTimeoutMs | int | integer >= 1 | ms | | TANSR_SERVE_TURN_WALL_CLOCK_MS | session | session.maxTurnWallClockMs | int | integer >= 1 | ms | | TANSR_SERVE_HOST_SHELL | session | session.hostShell | enum | posix|powershell | — | | TANSR_SERVE_SSE_MAX_BUFFER_BYTES | sse | sse.maxBufferBytes | int | integer >= 1 | bytes | | TANSR_SERVE_SSE_SLOW_POLICY | sse | sse.onSlowSubscriber | enum | disconnect|drop-oldest | — | | TANSR_SERVE_SSE_RETRY_MS | sse | sse.retryMs | int | integer >= 0 | ms | | TANSR_SERVE_LOG_FORMAT | log | logger.log | enum | json|text | — | | TANSR_SERVE_ON_UNHANDLED | process | processGuards.onFatal | enum | log|drain-exit | — | | TANSR_SERVE_READY_STREAM | process | readyFrame | enum | stdout|stderr|none | — | | TANSR_SERVE_CWD_ON_RESUME | factory | factory.cwdOnResume | enum | current|stored | — | | TANSR_SERVE_SESSION_MAX_AGE_DAYS | factory | factory.sessions.retention.maxAgeDays | int | integer >= 1 | days | | TANSR_SERVE_SESSION_MAX_PER_END_USER | factory | factory.sessions.retention.maxPerEndUser | int | integer >= 1 | — |

分组:admission(4) / http(1) / observability(2) / ring(1) / governance(5) / v2(1) / notify(2) / upstream(1) / session(5) / sse(3) / log(1) / process(2) / factory(3)。同表生成的部署样例:packages/server/.env.example

factory 组三键语义(表内只列形态与约束):TANSR_SERVE_CWD_ON_RESUME=current|storedcwdOnResume, resume 时存储 cwd ≠ 本次的处置——配了 cwdPolicy 的宿主缺省 stored(存储值重过策略闸, 策略拒 / 目录已失 → 409 cwd_unavailable{detail.reason}),未配缺省 current;current = 一键回退,十王修案 FX-C-08; TANSR_SERVE_SESSION_MAX_AGE_DAYS / TANSR_SERVE_SESSION_MAX_PER_END_USERsessions.retention.{maxAgeDays,maxPerEndUser}, 会话保留期(十王修案 FX-C-09 / C2-04):两者皆缺席 = 零删除 = 现状永存;任一在场即启用治理扫描第 ⑤ 段「store 保留」—— 每 sweep 周期经工厂 sweepRetention 触发、实扫间隔缺省 1 h,按 store meta.updatedAt 删超龄(maxAgeDays 缺席按 30 d 兜底, 与 api 会话租约窗同值;M-05:平台内置形装配过的 endUser 域以其 bundle governance.sessionRetentionDays(控制台 org 治理配置) 覆写该域超龄阈值,缺键域沿兜底;落盘(FX-C-09 收官):该天数随建档写入会话 meta.jsonretentionDays(resume 再装配后对账), 扫描逐会话优先按它判龄,serve 重启后零回退——收官前建的无键旧会话仍按兜底)/ 每 endUser 超数最旧的会话记录(附件 / 同居快照 / 独立根快照同删;在册活跃会话恒不删), 每轮有删除即记 session.retention_swept{scanned,deleted,failures,byReason};程序面另有 maxBytes / sweepIntervalMs 与手动 build.sweepSessionRetention({ force:true })

不在表内(语义归各自入口):TANSR_SERVE_TOKEN(/v1 Bearer)、 TANSR_SERVE_DRAIN_TIMEOUT_MS(drain 超时,缺省 30 s)、TANSR_SERVE_V1_RETENTION_MS / TANSR_SERVE_V1_MAX_RETAINED(SC-18)、TANSR_SERVE_MCP_CONNECTIONS(SC-31;缺省 1、上限 16。 SC-44 起只是进程级兜底层:mcpServers.<name>.pool.connections 逐台键 > 程序面 pool 选项 > 本 env

缺省 1,合并在 kernel governance.ts 单点;写了逐台键的服务器不受本 env 影响,有会话态的服务器 恒保持 1——见 doc/60 §2.5)。

部署参考件与优雅关闭时序(SC-32 / SC-32c;doc/118 §八 G1/G2/G10)

deploy/serve-v2/ 是 /v2 npm 嵌入形态的参考宿主部署件(examples/serve-demo 两入口打成单 文件镜像;compose N 副本 + nginx 分片路由/SSE 模板;K8s StatefulSet + 内层 nginx + ingress-nginx 注解 + NetworkPolicy);deploy/serve/ 是 CLI tansr serve /v1「一容器一智能体实例」模板。要点:

  • 本包不是网关:TLS 终结 / HTTP2 / IP 级限流 / WAF 归你的边缘(G10 反代前置);nginx 模板只做 SSE 直通(proxy_buffering off、读超时 ≥ 心跳 15 s × 4、对 text/event-stream 不压缩、 proxy_next_upstream off)与分片前缀路由,client_max_body_size 20m 与引擎体帽对齐。
  • 多副本 = 分片前缀路由 + 副本私有存储根(G2 先 (a) 档;SC-32b/SC-32c):会话运行态在副本内存 + 私有 store,/v2/sessions/<id>/… 必须回到创建副本。每副本持分片码 v2.sessionIdShard(0–255, = 副本序号;env TANSR_SERVE_SESSION_SHARD),引擎给全新会话生成的 sessionId 前两位十六进制 恒 = 分片码(其余 30 位仍是 UUID v4 随机位,形制仍是 UUID;resume/attach 沿用原 id),反代读 id 前两位做静态 map 路由回创建副本;创建请求(路径无 id)任意副本轮询。客户端零改动(sessionId 对客户端恒不透明,doc/98 §五-5.5),无 cookie、无端侧头、不按 Authorization 哈希(令牌刷新不 漂移)。不能按路径 sessionId 做一致性哈希——创建请求没有 sessionId,会话生在随机副本,后续按 sid 哈希落别处 → 404 → L3 resume 读不到 → 分叉;前缀路由把「id 决定副本」反过来变成「副本决定 id」。 副本序号即分片码:扩缩容不改既有映射;缩容副本上的会话随之丢失(与任何本地态服务同律,先 drain)。 自定义 AgentSessionFactory 须采用 init.sessionId,忽略即失去粘性并收 session.id_hint_ignored 告警。副本故障 = 其上会话不可迁移(客户端按 404 → 新建)。恒不共享存储目录(NFS/EFS/同一卷挂 两副本):kernel 会话锁按本机 PID 判活,容器 PID 命名空间下跨副本必然误判——SESSION_LOCKED 30 min 或并发写同一 journal → 500 store_corrupted;这是不安全配置而非「慢一点」。
  • 优雅关闭时序(宽限 45 s):t0 SIGTERM(+ K8s 摘 Endpoints)→ 拒新(/readyz 503、新建/resume 503 draining + Retry-After、SSE 下发长 retry:)→ 等在飞轮 ≤ TANSR_SERVE_DRAIN_TIMEOUT_MS (缺省 30 s,到点 interrupt(),轮末 store 提交照走)→ flush 落盘 → 停监听 ≤ 5 s + 连接收口 ≤ 5 s → 宿主 governor.close() → exit 0;t0+45 s 未退出 → SIGKILL。宽限恒 ≥ drain 超时 + 15 s。
  • 观测端点暴露:容器绑 0.0.0.0 时须 TANSR_SERVE_EXPOSE_OBSERVABILITY=1(参考镜像已设),探针 与抓取器走内网直连副本;反代恒不把 /metrics /healthz /readyz 转到公网。十王修案 FX-C-42(O-5): TANSR_SERVE_EXPOSE_METRICS 单独控 /metrics(优先于总开关);CLI tansr serve 入口把 /healthz /readyz 钉为恒开(探针面无机密,父进程 tansrd 池 / K8s 探活不受 EXPOSE_OBSERVABILITY=0 影响),库形态仍按 exposeHealth / exposeMetrics / expose 三选项自定。
  • 进程间就绪帧(FX-C-42;S2-04 / 候拍 ㉑):listening 后引擎恒额外写一行 TANSR_READY {"v":1,"url":"http://127.0.0.1:1234","pid":4242,"readyz":"/readyz"} 到 stdout——它不是日志(不经 ServeLogger、不受 TANSR_SERVE_LOG_FORMAT 影响、无 ts / level),是父子进程 协议(tansrd 池 readyFrom 第 ① 形;② server.listening JSON 行 / ③ 文案行为兜底,旧 serve 兼容一版)。 日志采集器按行严格 JSON 解析时 TANSR_SERVE_READY_STREAM=stderr 改道,嵌入式宿主 readyFrame:'none' 或 传写函数自定;formatReadyFrame / parseReadyFrame 是帧的唯一形态源(两端同笔)。
  • 容量估算(本机现状档不外推;doc/118 §一-1.1):单进程 ≈2500 活跃 SSE 连接时 ELD p99 ≈190 ms、 5000 连接 ≈250 ms;≈35 KiB 堆/会话(全装配链);企业档参考帽 maxActiveSessions 5000 / maxSseConnections 10000 / maxInflightBodyBytes 256 MiB / ELD p99 200 ms(G5,仅文档)。
  • fetchImpl 须尊重 init.signal:宿主自带 fetch(代理/埋点)包在 governor 外层或内层都可,但取消 传播链在此不得断——drain 到点的 interrupt()、每轮墙钟、分层超时都靠 AbortSignal 抵达上游。

扩展档位(横向扩展的三级;doc/118 §八 G2 / §11.2)

| 档 | 形态 | 状态 | 重放窗(Last-Event-ID)在哪 | |---|---|---|---| | (a) | 单进程 + 分片粘性:v2.sessionIdShard + 反代前缀路由,副本私有存储根,恒不共享目录 | 已落(SC-32b/c;上节) | 创建副本内存(MemorySessionEventLog = 环形缓冲,条数 1024 + 可选字节帽) | | (b) | 事件日志外置:AgentSessionsOptions.eventLog 注入 SessionEventLog 实现(Redis Streams / NATS JetStream 等) | 接口缝已落(SC-35);实现另包候需求,包名属命名面留用户 | 外部日志;任一副本可按 sessionId 重放,会话运行态(驱动/桥/在飞轮)仍在创建副本——分片路由仍需要 | | (c) | 会话态外置:驱动/桥/历史随会话迁移到任一副本 | 未立项 | 外部 |

(b) 档的缝:eventLog: (ctx: { sessionId, endUserId }) => SessionEventLog,每次纳管一枚;引擎的泵写入 (append(event, frameBytes?) 返回事件自带的 seq)、Last-Event-ID 重放与 gap 帧判定(readAfter(afterSeq) 一趟返回 { events, oldestRetainedSeq?, dropped, gap })、ringStats 汇总(stats())、记录离开内存 (close(),保留窗到点 / 数量帽 / resume 让位 / closeAll 各恰一次)只经此接口。缺席 = 内存实现,行为 逐字节同今(eventBufferSize / eventBufferMaxBytes 仍施于缺省实现;传了工厂即由实现定容量与逐出)。 实现纪律:全部方法同步(泵与订阅的同步原子块依赖此契约,异步后端须在接口之下自做写缓冲/本地镜像);seq 由句柄分配、随事件携带,日志恒不改写(外部存储的自生成 id 只能作游标);引擎恒不引 zod 之外第三方,任何 外置实现都在别的包里。

分层存储(冷层):会话历史落到 S3 / OSS / KV / 关系库(doc/119 IO-21 / doc/120;IO-32 集成后可用)

会话历史的耐久性地板恒是本地热层(轮末 journal 追加,fsync + rename ≈ 13 ms/轮);热层不可能配无限硬盘, 所以既有 store 工厂加了可选冷层:热层达到封段阈值(缺省 16 MiB / 5000 记录)后把这一截封成不可变的段对象 (JSONL + gzip,首尾哈希链)上传到你挂的存储,再以 CAS 重写一份清单(唯一提交点);已提交段可被热盘 LRU 逐出, 读侧按需回源。不挂冷层 = 今日行为,字节等价;客户端零感知(/v2 wire 零改动)。

五行接入(createAgentSessionFactory({ store }) 不改;doc/91 冻结接口不改):

import { createServeAgentSessionStore, createAgentSessionFactory } from '@tansr/serve';
import { createS3BlobStore } from './store-s3.js';           // 你的适配器(参考实现 examples/store-s3,零依赖 fetch + SigV4)

const store = createServeAgentSessionStore({
  dir: '/var/lib/tansr/hot',                                   // 热层:本地盘,恒不共享
  cold: createS3BlobStore({ bucket, region, credentials }),   // 一级 SegmentBlobStore(或二级 SessionHistoryStore)
  policy: { tenant: 'acme', hotRetention: { maxBytes: 20 * 2 ** 30 } },
  logger,                                                      // 冷层事件 → 结构化日志 store.*
});
const build = createAgentSessionFactory({ cwd, store, platform: { ... } });

两级选择:一级 SegmentBlobStore(5 个字节方法 put / get / head / list / delete + 一次能力自述 capabilities;S3 / OSS / COS / MinIO / R2 / GCS / Azure / GridFS / 文件系统皆此级,引擎内置分段、编码、清单 CAS、 热盘 LRU、重试、指标)——推荐;二级 SessionHistoryStore(store / load / listSessions / remove,自管布局; KV / 关系库更贴合)。适配器唯一要遵守的错误约定是 StoreError.kind 四类(transient / permanent / not_found / precondition_failed;其他异常一律按 transient)。一级冷层 conditionalPut:falselist:'none'必须再挂 index: SessionIndexStore(多副本共享会话元 / CAS 落点),否则构造期 fail-fast(不静默降级为不安全提交)。 随包实现:createMemoryBlobStore()(测试替身)、createFsBlobStore({ dir })(目录模拟对象存储;只可作冷层)。 一致性测试套 runStorageConformance(store) 认证你的适配器(fs / memory / S3 同一套)。

旋钮(policy):

| 键 | 缺省 | 语义 | |---|---|---| | codec | 'jsonl+gzip' | 段编码;'jsonl+zstd' 仅 Node ≥ 22.15 可用 | | hotRetention.maxBytes | 不设 | 热盘上本地段文件占用帽:已提交段先逐出;仍越帽 → readiness() 红 + store.hot_full 事件;未上传段恒不删 | | hotRetention.keepRecentSegments | 引擎缺省 | 逐出时保留的尾段数(压缩后 resume 常只需摘要 + 尾段) | | upload.mode / concurrency | 'async' / 4 | 热层先 ack 再异步上传;并发受约束(A12) | | upload.retryBudget | 8 次 / 退避封顶 60 s | transient 重试预算;耗尽 → store.commit_failed{gaveUp:true} + onStoreError | | restore.prefetchTailSegments | 1 | 热层全失重建后预取的尾段数 | | transform | 无 | encode / decode 字节流挂点:加密 / 脱敏归你(引擎不做密码学) | | keyMapper | 恒等 | 规范键 <tenant>/<endUserKey>/<sessionId>/<volumeId>/… → 你的键(不得破坏唯一性) | | tenant | 'default' | 规范键首段(多租户隔离) |

metrics 缺省 kernel defaultMetrics(与 /metrics 同源);index 见上;onStoreEvent(event) 原样订阅冷层事件; onStoreError(sessionId, error) 只在放弃时回调(热层已 ack,恒不抛给调用方)。

失败语义:上传 / 清单提交失败恒不回滚热层(轮末已 ack 的历史耐久不变)——按预算重试,超预算记 store.commit_failed + 积压指标,段留在热盘、下次绑定时对账补传;precondition_failed(清单被另一副本接管)本写者 放弃不重试(A2 / A13)。读侧:回源段篡改 / 截断 / 跳段(permanent)→ 500 store_corrupted,恒不静默续接;冷层 暂不可达(transient)原样上抛 StoreError(不冒充损坏)。热层全失(节点重建,A10):同一冷层重开 store, GET /v2/sessions 列表见会话、resume 经冷清单 → 段 → 重灌成功(只有已封存的段可恢复;未越阈值的活段随热层丢失—— 需要更细粒度的耐久点请调小 segmentation 阈值)。delete 先删冷层再删热层。

两条告警线(/metrics,tansr_kernel_store_* 族:ops_total{op,result} / bytes_total{op} / latency_ms{op} / failures_total{kind} / backlog_bytes / hot_bytes): ① tansr_kernel_store_backlog_bytes 持续增长 = 冷层不可达或写失败(热层仍在服务,但保留窗只剩本地盘); ② tansr_kernel_store_hot_bytes 逼近 hotRetention.maxBytes = 即将触顶——触顶后 /readyz 503(理由 store_hot_full,v2.store.readiness 经工厂 storeReader 转发即自动接线;或把 store.readiness 直接挂 observability.readinessProbes),LB 摘流、新建改落别的副本。结构化日志:store.segment_sealed / store.segment_committed / store.commit_failed{kind,attempts,gaveUp} / store.hot_full / store.hot_recovered / store.evicted / store.orphan_gc(字段恒不含消息内容)。

部署建议:热层恒本地盘、恒不共享(共享盘是不安全配置,见上节);冷层用生命周期规则承接保留窗 (<tenant>/<endUserKey>/<sessionId>/<volumeId>/ 前缀按 expiresAt / 标签过期;压缩换卷 = 新 volumeId,旧卷独立 过期不影响新卷);冷层里的段是明文 JSONL(压缩)——加密、密钥管理、驻留地、访问控制归你(transform 挂点), endUserKey 是哈希不是身份,不要把 endUserId 明文放进键或标签;多副本下配 index(会话元共享、list 便宜), policy.tenant 按租户隔离前缀。优雅关闭:drain 第三阶段在工厂 flush 之后再 await store.flush({ timeoutMs }) 等冷层队列排空(返回 false = 超时仍有积压,热层已耐久,重启后对账补传)。

三观测端点(免 Bearer;缺省只在回环监听时暴露)

| 端点 | 语义 | 响应 | |---|---|---| | GET /healthz | liveness:进程活着即 200(关闭期亦 200,摘流看 readyz) | {"status":"ok"} | | GET /readyz | readiness:!closing ∧ 全部就绪探针 ready | 200 {"ready":true,"reasons":[]} / 503 {"ready":false,"reasons":["closing",…]} | | GET /metrics | OpenMetrics 1.0 文本(application/openmetrics-text; version=1.0.0) | 内核 + serve 指标一份 exposition,末行 # EOF |

暴露纪律:三端点免 Bearer,所以暴露面必须显式——host 为回环(127.x / localhost / ::1)时缺省暴露;绑 0.0.0.0 / 公网地址时缺省不暴露,须 observability: { expose: true } (或分别 exposeHealth / exposeMetrics)。未暴露时这三条路径落回既有流程,与任意 未知路径响应完全一致(无 Bearer 401 / 有 Bearer 404),不泄漏存在性。容器/K8s 部署把 它们只发布给探针与抓取器所在网络(反代前置时不要把 /metrics 转到公网)。

const server = await startServer({
  // ...
  observability: {
    expose: true, // 绑 0.0.0.0 时显式放开
    readinessProbes: [() => (storeWritable ? true : { ready: false, reason: 'store_unwritable' })],
    requestLogSampleRate: 0.1, // 高并发下调请求日志采样;错误级事件恒不采样
  },
});
server.stats(); // 结构化快照:sessions{active,byFace,pending?,retainedEnded?} / sse / http / eventLoopDelayMs / memory / notifier? / readiness

指标(首批;名恒 tansr_ 前缀,process_resident_memory_bytes 为唯一标准名例外)

内核层(五形态共享,TUI/Headless/serve/acp/SDK 同一份进程级读数;kernel 出口 defaultMetrics / createMetricsRegistry / renderOpenMetrics 经本包再出口): tansr_kernel_model_calls_total{result}tansr_kernel_model_call_duration_mstansr_kernel_tool_executions_total{result}tansr_kernel_tool_duration_mstansr_kernel_turns_total{reason}tansr_kernel_internal_errors_totaltansr_kernel_compactions_total{mechanism}tansr_kernel_retries_total{class}

serve 层:tansr_serve_http_requests_total{method,route,status}(route 恒模板化,如 /v2/sessions/:id/events;status="0" = 响应头未发出即断开)、 tansr_serve_http_request_duration_ms{route}tansr_serve_rejections_total{code} (401/403/404/405/409/410/413/422/429/4xx/5xx)、tansr_serve_sessions_active{face=v1|v2}tansr_serve_sessions_pendingtansr_serve_sessions_retained_endedtansr_serve_sse_connections_activetansr_serve_sse_frames_written_totaltansr_serve_replay_gap_totaltansr_serve_ring_evictions_totaltansr_serve_event_loop_delay_ms{stat=p50|p99|max}(两次采集之间的窗口)、 process_resident_memory_bytestansr_serve_webhook_deliveries_total{result}tansr_serve_store_commit_errors_total / tansr_serve_store_create_errors_totaltansr_serve_token_mint_failures_totaltansr_serve_token_invalidations_total{reason}(FX-C-15:上游 401 裁定令牌失效,按信封 detail.reason 分键;每次伴随失效 + 重铸 + 重试恰一次)。

纪律:指标标签恒不含 sessionId / endUserId / 令牌 / 消息内容(高基数与凭据禁令); 未知路径一律 route="other"/v2route 模板与路由分发同出一表 V2_ROUTE_TABLE(导出;21 端点 × { method, action(模板), pattern, endpoint },FX-C-45)——此前观测层手写 7 动作,compact / checkpoints 全族 / cwd / audio 九条路径落 other;今 other 只对域内 404 出现(锁卷 observability-route-coverage.test.ts)。 同进程起多台 startServer 且共享缺省注册表时,gauge 族由最后 采集者覆写、counter 族累加——需按实例隔离读数时各传 observability.metrics: createMetricsRegistry()

结构化日志与 request-id

ServeLogger 新增可选 log(level, event, fields)(旧 info/error 保留)。事件名恒英文 机器码(LOG_EVENT 词表:request.completed / request.unhandled / sse.opened / sse.closed{reason} / store.commit_failed / store.create_failed / notify.delivery_failed / server.listening / server.closed …;分层存储冷层在场时另有 store.segment_sealed / store.segment_committed / store.hot_full / store.hot_recovered / store.evicted / store.orphan_gc, 见上节),字段恒不含令牌与消息内容(sessionId 可在场, endUserId 仅 /v2 已鉴权时由路由层回填)。只有旧形 info/error 的 logger 会收到回落: 带人读文案的事件(listening/closed/兜底错误)仍是既有 i18n 文案行,其余事件为一行 JSON {"level","event",...fields}

request-id:每个请求采纳合法入站 x-request-id(^[A-Za-z0-9._-]{1,128}$),否则取 W3C traceparent 的 trace-id,再否则自铸 UUID;恒回响应头 x-request-id(含 401/404), 并作 request.completed{requestId, method, route, status, durationMs}request.unhandled 的关联键。SSE 长连接在连接关闭时发一次 request.completed(sse:true)。 createAgentSessionFactoryonStoreError 缺省不再静默:一行结构化 JSON 到 stderr 并计数。

容量基线与门禁

bench/serve/(A/B/C 路审计脚本)已门禁化:pnpm bench:serve:gatebench/serve/thresholds.json 现状档断言(本机数字为参考、不外推;目标档随 G12 回填),详见 bench/serve/README.md

容量模型:「挂着的会话数」≠「同时活跃数」——订阅者全部离场 orphanGraceMs(60 s)后中止当前轮、再 idleAfterGoneMs(5 min)后落盘退出内存,resume 复活;内存与 CPU 只随同时活跃的会话增长。单进程天花板是 单事件循环 CPU(几千会话同时流式),准入帽(admission)让它有界拒绝而非拖垮,v2.sessionIdShard 分片前缀 路由随时加副本。部署前置:每条 SSE 占 1 个文件句柄,Linux 缺省 nofile 1024 会把并发订阅者卡在一千出头 并报 EMFILE——按目标并发 ≥ 2× 抬(deploy/serve-v2 模板已置 65536,nginx 侧 worker_connections 同律)。

上游治理(平台内置形:铸令牌闸 / bundle 缓存 / 503 语义 / governor 注入)

平台内置形(createAgentSessionFactory({ platform }))的上游链是 appid/appkey → POST /v1/app-tokens(per-endUser 令牌)→ GET /t1/config + POST /t1/heartbeat (bundle/features)→ POST /t1/exchange(模型)。Wave 2(SC-22/23/24/30)起这条链有了治理位; 缺省值全部为建议值(候 G3 拍板),upstreamStats() 可读:

const build = createAgentSessionFactory({
  cwd,
  logger,                          // 缺省提示帧承接经 platform.warning 结构化事件(缺席回落 console.warn)
  platform: {
    apiBaseUrl, appId, appKey,
    tokenMinter: {                 // 铸令牌治理(SC-23/SC-30;全部可选)
      maxConcurrentMints: 32,      // 全局并发闸:同时出网的 /v1/app-tokens 数(重启惊群从 N 降到闸值)
      maxQueuedMints: 1024,        // 闸等待队列;满额即刻 503 overloaded(不出网)
      mintQueueTimeoutMs: 10_000,  // 排队超时 → 503 overloaded
      expiryJitterRatio: 0.15,     // 令牌 expiresAt 只提前不推后的抖动比(打散 TTL−300 s 的对齐刷新)
      negativeCacheInitialMs: 5_000, negativeCacheMaxMs: 60_000, // 失败负缓存 5 s 起指数 ×2 至 60 s
      maxCachedTokens: 100_000,    // 令牌缓存 LRU 帽(过期项读到即删)
    },
    bundleCacheTtlMs: 60_000,      // bundle/features 缓存 TTL 上界(SC-24;0 = 每会话拉取)。也可传 bundleCache 实例跨工厂共享
    governor,                      // SC-22:providers createUpstreamGovernor 产物;三处出站同一枚(见下)
  },
});
build.upstreamStats?.();           // { tokenMinter: { inflight, queued, mints, hits, negativeHits, staleServed, failures{kind}, cached, evicted }, bundleCache?: { hits, misses, revalidations, notModified, inflightDedup, … } }
  • 铸令牌闸 / 抖动 / 负缓存:500 endUser 同时首连时 /v1/app-tokens 在飞峰值 = 闸值(此前 500); 同批铸出的令牌不再同刻进入刷新窗;某 endUser 铸造失败后窗内不再出网(直接回同一错), 刷新失败但旧令牌仍在时效内直接沿用旧令牌;warm(endUserIds) 可在启动期预热。
  • 令牌失效反馈(十王修案 FX-C-15 / R-02):平台对携 x-tansr-app-token 的请求(模型 exchange / bundle / 系统工具的平台提供方)回 401 { error: { code: 'unauthorized', detail: { reason } } } 时,刷新 fetch 在同一请求内 minter.invalidate(仅当缓存现值仍是本次所携令牌)→ 重铸 → 重发恰一次;重试仍 401 则原样交回(每请求至多 一次重铸,恒不循环)。reason === 'check_unavailable'(平台校验依赖瞬态不可用)不失效、不重铸;其余码 / 403 / 无信封亦不触发。此前吊销令牌后 serve 拿旧令牌 ≈ 55 min 恒 401。计数:stats().invalidations{reason} (只计真正逐出)+ 指标 tansr_serve_token_invalidations_total{reason}(按 401 裁定计;自实现 minter 缺 invalidate 时只计不重试)。失效码集合 TOKEN_INVALIDATING_CODES = ['unauthorized'](导出;M-13 起改由 openapi x-invalidates-token 生成到 sdk,serve import)。
  • bundle 缓存:键 (apiBaseUrl, appId, scope, featuresFingerprint)(FX-C-16:径别 app_user | owner + 客户端 features 声明指纹;serve 恒令牌径,宿主自有 owner 径消费方共享同一实例亦不串档),TTL = min(平台 max-age, bundleCacheTtlMs);过期后 If-None-Match 条件重拉(304 只续期);features(heartbeat)按 endUser 每 TTL 一次而非每会话。 建会话上游往返:冷 endUser 3 → 1(仅铸令牌;首次 +1 config),热 endUser 2 → 0。 一致性 SLA:控制台改模型授权/别名后在网关生效的延迟上界 = bundleCacheTtlMs(缺省 60 s); 要更快生效就调小(平台 max-age 仍是下界之一),要即时一致传 0
  • 503 upstream_unavailable(候 doc/98 §七 增笔 G4-a):平台铸令牌撞 429 / 5xx / 网络错时建会话 回 503 upstream_unavailable 并携 Retry-After(取平台 Retry-After,缺席按 admission.retryAfterMs 5 s);客户端按 5xx 语义退避重试即可。与 503 overloaded(本机闸忙 / 准入帽)分开:一个是「上游坏, 加压无益」,一个是「本机忙,稍后再来」。4xx(凭据/入参被拒)维持 400 create_failed(重试无益)。 指标 tansr_serve_token_mint_failures_total 计每次上游铸造失败(负缓存命中不重复计)。
  • governor 注入(SC-22 serve 半场):治理核(显式 undici.Agent 连接上限 / per-origin 隔离舱 / 熔断 / 重试预算 / 本地令牌桶)落 @tansr/providers createUpstreamGovernor(options);serve 只负责把 同一枚进程级实例的 fetch 注入三处出站——platform.governor(铸令牌器 + bundle 缓存 + 模型 client)与 startServer({ governor })(轮末 webhook 通知机)。推荐:一进程一枚,connections 按平台 连接配额、keepAliveTimeout ≥ 60 s;宿主持有其生命周期(governor.close()server.drain() 之后 由宿主调用,serve 恒不代关);宿主自实现 fetch 时必须尊重 init.signal(取消传播链在此不得断)。 未传 governor = 直连 fetch(行为零漂移)。
  • G3 平台限流上报:平台现行限流键绑 app key(/t1/exchange 120/min)+ 源 IP(600/min);网关形态 下全部 endUser 共用一把 key、一个出口 IP,本节的闸/缓存只能削峰、不能抬顶——upstream_unavailable 持续出现且 Retry-After 对齐到分钟整点,即触顶信号,应上报平台侧调档(总卷 G3 / C 路 D-1)。

应用系统提示词与移动端(AP-SP)

平台内置形的 platform.system 为宿主业务段;未设置时使用平台应用 systemPrompt。默认 fallback 下显式值(含 [])替换平台段;平台 systemPromptPolicy=prepend 时保留平台段在前,再拼宿主段,[] 也不会移除平台段。platform.systemAppend 追加宿主工具使用指南,不触发覆盖。P 与 S 是同一 system 层输入,拼接不是权限或冲突裁决机制。

Android/iOS 经 /v2 连接本服务,用户 prompt 不是 system 字段;配置放在平台或 Node 宿主,客户端不传 appkey。平台保存正文或策略后,已有会话在下一轮开始前按 ETag 验证配置,绕过 60 秒装配缓存;会话历史继续保留,正在执行的一轮(含工具多步)使用该轮提示词快照。宿主 S/A 仍在建会时固定;恢复历史不阻止下一轮刷新。刷新失败会以 application_prompt_refresh_failedturn.aborted 中止本轮,不带旧配置发起模型调用;刷新默认最多等待 30 秒,可取消后重试。工具、权限、模型和计价等其他配置仍沿原装配生命周期。示例及完整矩阵见 serve-demo提示词指南。此变更尚需正式发布包含 AP-SP 的版本。

MEDIA-03 历史媒体恢复

媒体产物随工具回执保存,恢复后的 result 可复用现有媒体组件;artifactUnavailable 单独表示预览不可恢复,不代表工具失败。首次写产物会启用存储版本 2,请统一升级写者并先备份。配置、TS 示例及英文说明见媒体历史恢复 / Media history recovery。本能力已随 @tansr/[email protected] 发布,须同时升级消费端。