dsh-session-messaging 0.3.0 → 0.4.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/CHANGELOG.md +15 -0
- package/README.md +14 -3
- package/README.zh-CN.md +14 -3
- package/lib/config.d.ts +4 -0
- package/lib/config.js +1 -0
- package/lib/protocol.d.ts +3 -1
- package/lib/service.d.ts +20 -1
- package/lib/service.js +61 -14
- package/lib/tools.js +3 -1
- package/package.json +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.4.0] - 2026-10-18
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Cold-session delivery: when a target session is not live, the plugin resumes
|
|
12
|
+
it through the deployment's `sessionController` before delivering. Enable or
|
|
13
|
+
disable it with the new `allowColdDelivery` config field (on by default).
|
|
14
|
+
- `session_send` receipts now report `delivery: 'live' | 'resumed'`.
|
|
15
|
+
- New `cold-delivery-failed` error code for a refused or failed resume.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Profiles without a session controller keep the previous `target-not-live`
|
|
20
|
+
refusal; archived/subagent-owned sessions are refused by the controller.
|
|
21
|
+
|
|
7
22
|
## [0.3.0] - 2026-10-18
|
|
8
23
|
|
|
9
24
|
### Added
|
package/README.md
CHANGED
|
@@ -56,7 +56,8 @@ The sidebar entry opens a small panel: toggle cross-directory messaging (persist
|
|
|
56
56
|
- Advisory **write-scope conflict detection**: if a peer declared an overlapping path, the receipt and the message envelope carry a `conflict-warning`.
|
|
57
57
|
- No new Session event types and no `SESSION_FORMAT_VERSION` change: the relay is a durable `user/message` with a declared `session-message` source.
|
|
58
58
|
- Bilingual plugin-card metadata and icons (English / 简体中文) for the dsh plugin manager.
|
|
59
|
-
-
|
|
59
|
+
- **Cold-session delivery** — a persisted session that is not live is resumed through the Web/Desktop `sessionController` before delivery (`allowColdDelivery`, on by default).
|
|
60
|
+
- Bounded: message size, per-sender→target rate limit, top-level-only, self-send rejected. Cross-directory peers are allowed by default; `sameDirectoryOnly: true` restricts to one working directory.
|
|
60
61
|
|
|
61
62
|
## How it works
|
|
62
63
|
|
|
@@ -87,7 +88,7 @@ The envelope always states that the message comes from another session, not the
|
|
|
87
88
|
dsh plugin --profile <profile> add dsh-session-messaging
|
|
88
89
|
```
|
|
89
90
|
|
|
90
|
-
The package is published as [`dsh-session-messaging`](https://www.npmjs.com/package/dsh-session-messaging) (latest `0.
|
|
91
|
+
The package is published as [`dsh-session-messaging`](https://www.npmjs.com/package/dsh-session-messaging) (latest `0.4.0`). For library use: `npm install dsh-session-messaging`.
|
|
91
92
|
|
|
92
93
|
### From GitHub
|
|
93
94
|
|
|
@@ -103,6 +104,15 @@ The repository ships the built `lib/` output, so a git install does not run a bu
|
|
|
103
104
|
dsh plugin --profile <profile> add /absolute/path/to/dsh-session-messaging
|
|
104
105
|
```
|
|
105
106
|
|
|
107
|
+
### Troubleshooting: switching from a link install
|
|
108
|
+
|
|
109
|
+
If a profile previously linked a local checkout and later installed the npm package **while the app was running**, the running Loader can still reference the old checkout path. Removing that checkout's `node_modules` then surfaces `ERR_MODULE_NOT_FOUND` for `@deepseek-ai/*` peers even though the npm package is installed. Remove and reinstall the bundle, then restart the app (a page reload is enough for the client half):
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
dsh plugin --profile <profile> remove dsh-session-messaging
|
|
113
|
+
dsh plugin --profile <profile> add dsh-session-messaging@latest
|
|
114
|
+
```
|
|
115
|
+
|
|
106
116
|
### Desktop app
|
|
107
117
|
|
|
108
118
|
The macOS/Windows desktop app uses its own profiles (for example `desktop`). Install with the bundled CLI or the agent's `plugin_manager` tool, targeting that profile. In dsh `0.2.1-rc.2` the configuration reloads through HMR, so the two plugin rows usually activate without restarting the app.
|
|
@@ -148,13 +158,14 @@ The bundle row accepts validated configuration; every field has a default:
|
|
|
148
158
|
requireTargetOptIn: false # sessions are open to peers by default
|
|
149
159
|
sameDirectoryOnly: false
|
|
150
160
|
allowSelfSend: false
|
|
161
|
+
allowColdDelivery: true # resume a persisted target when it is not live
|
|
151
162
|
```
|
|
152
163
|
|
|
153
164
|
## Security model and limitations
|
|
154
165
|
|
|
155
166
|
- **Peer content is untrusted input.** It enters the target session as a user message. The envelope labels the sender and warns the model, but dsh has no content-approval point for peer messages; normal tool approvals, `agent/pre-step`, and sandbox policy remain the only hard boundaries.
|
|
156
167
|
- **Top-level sessions only, in both directions.** A subagent session is neither listed nor allowed to send (`caller-not-top-level`), and a top-level session cannot target a subagent (`target-not-top-level`). Subagents keep using the existing parent/child `send_message` channel. The envelope `from` field is the trusted identity; any identity claim in the message body is data only.
|
|
157
|
-
- **
|
|
168
|
+
- **Cold sessions are resumed, not queued.** When the target is not live and `allowColdDelivery` is on, the plugin calls the deployment's `sessionController.resolveAgent()`, which resumes that session and may start a model turn (token cost). Profiles without that controller still answer `target-not-live`. Archived or subagent-owned sessions are refused by the controller/session gate. Cross-process/cross-host messaging remains unimplemented.
|
|
158
169
|
- **Advisory scopes, not locks.** dsh has optimistic filesystem version guards (`FS_STALE_VERSION`) but no file locking. `writeScopes` and `conflict-warning` are coordination aids; Bash, formatters, and code generators bypass them.
|
|
159
170
|
- **In-memory state.** Reception pauses and declared scopes live for the plugin's lifetime and reset on reload/restart.
|
|
160
171
|
- **Cross-directory by default.** Sessions in different working directories can message each other. Set `sameDirectoryOnly: true` to restore a path boundary; peer content remains untrusted either way.
|
package/README.zh-CN.md
CHANGED
|
@@ -52,7 +52,8 @@ macOS 桌面客户端(`0.2.1-rc.2`)真实运行:另一个用户启动的
|
|
|
52
52
|
- 建议性**写范围冲突检测**:与 peer 声明重叠时,回执与消息信封带 `conflict-warning`。
|
|
53
53
|
- 不新增 Session 事件类型、不改 `SESSION_FORMAT_VERSION`:消息就是带 `session-message` source 的 `user/message`。
|
|
54
54
|
- 插件管理卡片提供中英双语标题、简介与图标(English / 简体中文)。
|
|
55
|
-
-
|
|
55
|
+
- **冷会话投递**:目标不在 live 列表时,通过 Web/Desktop 的 `sessionController` 恢复持久会话后再投递(`allowColdDelivery`,默认开启)。
|
|
56
|
+
- 有边界:长度上限、每 sender→target 限频、仅顶层、拒绝自发送。默认允许跨目录对等会话;`sameDirectoryOnly: true` 可收紧为同工作目录。
|
|
56
57
|
|
|
57
58
|
## 工作原理
|
|
58
59
|
|
|
@@ -83,7 +84,7 @@ macOS 桌面客户端(`0.2.1-rc.2`)真实运行:另一个用户启动的
|
|
|
83
84
|
dsh plugin --profile <profile> add dsh-session-messaging
|
|
84
85
|
```
|
|
85
86
|
|
|
86
|
-
包已发布为 [`dsh-session-messaging`](https://www.npmjs.com/package/dsh-session-messaging)(最新 `0.
|
|
87
|
+
包已发布为 [`dsh-session-messaging`](https://www.npmjs.com/package/dsh-session-messaging)(最新 `0.4.0`)。库用法:`npm install dsh-session-messaging`。
|
|
87
88
|
|
|
88
89
|
### 从 GitHub 安装
|
|
89
90
|
|
|
@@ -99,6 +100,15 @@ dsh plugin --profile <profile> add github:XBXyftx/dsh-session-messaging
|
|
|
99
100
|
dsh plugin --profile <profile> add /absolute/path/to/dsh-session-messaging
|
|
100
101
|
```
|
|
101
102
|
|
|
103
|
+
### 排障:从本地 link 切换到 npm 安装
|
|
104
|
+
|
|
105
|
+
如果 profile 之前 link 了本地源码,又在**应用运行中**安装了 npm 包,运行中的 Loader 可能仍引用旧源码路径;此时删除源码目录的 `node_modules` 会出现 `@deepseek-ai/*` 的 `ERR_MODULE_NOT_FOUND`,即使 npm 包已经安装。先移除再重装 bundle,然后重启应用(客户端部分刷新页面即可):
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
dsh plugin --profile <profile> remove dsh-session-messaging
|
|
109
|
+
dsh plugin --profile <profile> add dsh-session-messaging@latest
|
|
110
|
+
```
|
|
111
|
+
|
|
102
112
|
### 桌面应用
|
|
103
113
|
|
|
104
114
|
桌面应用使用自己的 profile(例如 `desktop`),用其内置 CLI 或 agent 的 `plugin_manager` 工具安装到该 profile。dsh `0.2.1-rc.2` 会通过 HMR 重载配置,通常无需重启应用即可激活。
|
|
@@ -142,13 +152,14 @@ dsh plugin --profile <profile> add /absolute/path/to/dsh-session-messaging
|
|
|
142
152
|
requireTargetOptIn: false # 默认对 peer 开放
|
|
143
153
|
sameDirectoryOnly: false
|
|
144
154
|
allowSelfSend: false
|
|
155
|
+
allowColdDelivery: true # 目标不 live 时先恢复持久会话
|
|
145
156
|
```
|
|
146
157
|
|
|
147
158
|
## 安全模型与限制
|
|
148
159
|
|
|
149
160
|
- **peer 内容是可信边界外的输入**:它以 user 消息进入目标会话,信封会标注来源并警告模型,但 dsh 没有针对 peer 消息的内容审批点;工具审批、`agent/pre-step` 与 sandbox 策略仍是唯一的硬边界。
|
|
150
161
|
- **只允许顶层会话,收发两个方向都对称**:subagent 会话既不会出现在 peers 里,也不允许发送(`caller-not-top-level`);顶层会话也不能把 subagent 作为目标(`target-not-top-level`)。subagent 继续使用既有的父子 `send_message` 通道。消息信封的 `from` 字段才是可信身份;正文里的任何身份自称都只是数据。
|
|
151
|
-
-
|
|
162
|
+
- **冷会话是“恢复后投递”,不是排队**:目标不 live 且 `allowColdDelivery` 开启时,插件调用部署的 `sessionController.resolveAgent()` 恢复该会话,可能启动模型 turn(产生 token 成本)。没有该服务的 profile 仍返回 `target-not-live`;归档或 subagent 拥有的会话会被控制器/会话门禁拒绝。跨进程/跨机器通信仍未实现。
|
|
152
163
|
- **写范围只是建议,不是锁**:dsh 只有乐观版本守卫(`FS_STALE_VERSION`),没有文件锁;Bash、格式化器、代码生成器会绕过写范围提示。
|
|
153
164
|
- **状态在内存**:暂停接收与写范围声明随插件生命周期存在,重载/重启后重置。
|
|
154
165
|
- **默认允许跨目录**:不同工作目录的会话也可以互发消息。设置 `sameDirectoryOnly: true` 可恢复路径边界;无论是否限制,peer 内容都不可信。
|
package/lib/config.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface SessionMessagingConfig {
|
|
|
21
21
|
readonly sameDirectoryOnly: boolean;
|
|
22
22
|
/** Allow a session to relay to itself (diagnostics only; off by default). */
|
|
23
23
|
readonly allowSelfSend: boolean;
|
|
24
|
+
/** Resume a persisted, non-live session through sessionController before delivery. */
|
|
25
|
+
readonly allowColdDelivery: boolean;
|
|
24
26
|
}
|
|
25
27
|
/** Schemastery schema for the plugin row's `config` block. */
|
|
26
28
|
export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
@@ -29,12 +31,14 @@ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
|
29
31
|
requireTargetOptIn: Schema<boolean, boolean, "defined">;
|
|
30
32
|
sameDirectoryOnly: Schema<boolean, boolean, "volatile-defined">;
|
|
31
33
|
allowSelfSend: Schema<boolean, boolean, "defined">;
|
|
34
|
+
allowColdDelivery: Schema<boolean, boolean, "defined">;
|
|
32
35
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
33
36
|
maxMessageChars: Schema<number, number, "defined">;
|
|
34
37
|
maxMessagesPerMinute: Schema<number, number, "defined">;
|
|
35
38
|
requireTargetOptIn: Schema<boolean, boolean, "defined">;
|
|
36
39
|
sameDirectoryOnly: Schema<boolean, boolean, "volatile-defined">;
|
|
37
40
|
allowSelfSend: Schema<boolean, boolean, "defined">;
|
|
41
|
+
allowColdDelivery: Schema<boolean, boolean, "defined">;
|
|
38
42
|
}>>, "plain">;
|
|
39
43
|
/** Plugin runtime config: volatile fields arrive as stable references. */
|
|
40
44
|
export interface ResolvedSessionMessagingConfig extends Omit<SessionMessagingConfig, 'sameDirectoryOnly'> {
|
package/lib/config.js
CHANGED
|
@@ -13,4 +13,5 @@ export const Config = Schema.object({
|
|
|
13
13
|
requireTargetOptIn: Schema.boolean().default(false),
|
|
14
14
|
sameDirectoryOnly: Schema.boolean().default(false).volatile(),
|
|
15
15
|
allowSelfSend: Schema.boolean().default(false),
|
|
16
|
+
allowColdDelivery: Schema.boolean().default(true),
|
|
16
17
|
});
|
package/lib/protocol.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare const MAX_WRITE_SCOPES = 32;
|
|
|
17
17
|
/** Maximum length of one declared write scope. */
|
|
18
18
|
export declare const MAX_WRITE_SCOPE_CHARS = 200;
|
|
19
19
|
/** Stable refusal reasons surfaced to callers and tests. */
|
|
20
|
-
export type MessagingErrorCode = 'invalid-request' | 'caller-not-top-level' | 'self-send' | 'target-not-live' | 'target-not-top-level' | 'target-not-opted-in' | 'target-paused' | 'cross-directory' | 'rate-limited' | 'delivery-rejected';
|
|
20
|
+
export type MessagingErrorCode = 'invalid-request' | 'caller-not-top-level' | 'self-send' | 'target-not-live' | 'target-not-top-level' | 'target-not-opted-in' | 'target-paused' | 'cold-delivery-failed' | 'cross-directory' | 'rate-limited' | 'delivery-rejected';
|
|
21
21
|
/** Typed refusal that a tool call surfaces as an error result. */
|
|
22
22
|
export declare class MessagingError extends Error {
|
|
23
23
|
readonly code: MessagingErrorCode;
|
|
@@ -43,6 +43,8 @@ export interface SendReceipt {
|
|
|
43
43
|
readonly targetStatus: 'idle' | 'running';
|
|
44
44
|
/** Advisory overlaps with peers' declared write scopes; empty means none detected. */
|
|
45
45
|
readonly conflicts: readonly ScopeConflict[];
|
|
46
|
+
/** Whether delivery used a live Agent or resumed a persisted session first. */
|
|
47
|
+
readonly delivery: 'live' | 'resumed';
|
|
46
48
|
}
|
|
47
49
|
/** One same-workspace peer as the model sees it. */
|
|
48
50
|
export interface PeerView {
|
package/lib/service.d.ts
CHANGED
|
@@ -37,12 +37,14 @@ export declare class SessionMessagingService extends Service {
|
|
|
37
37
|
requireTargetOptIn: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
38
38
|
sameDirectoryOnly: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
|
|
39
39
|
allowSelfSend: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
40
|
+
allowColdDelivery: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
40
41
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
41
42
|
maxMessageChars: import("@deepseek-ai/schemastery").default<number, number, "defined">;
|
|
42
43
|
maxMessagesPerMinute: import("@deepseek-ai/schemastery").default<number, number, "defined">;
|
|
43
44
|
requireTargetOptIn: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
44
45
|
sameDirectoryOnly: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
|
|
45
46
|
allowSelfSend: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
47
|
+
allowColdDelivery: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
46
48
|
}>>, "plain">;
|
|
47
49
|
private readonly config;
|
|
48
50
|
private readonly limiter;
|
|
@@ -80,7 +82,24 @@ export declare class SessionMessagingService extends Service {
|
|
|
80
82
|
*/
|
|
81
83
|
private assertTopLevelCaller;
|
|
82
84
|
/**
|
|
83
|
-
*
|
|
85
|
+
* Enforce per-target consent and the sender→target rate limit once.
|
|
86
|
+
* @param caller - sending agent.
|
|
87
|
+
* @param targetId - target session identity.
|
|
88
|
+
*/
|
|
89
|
+
private checkConsentAndRate;
|
|
90
|
+
/**
|
|
91
|
+
* Resume a persisted, non-live session through the deployment's session
|
|
92
|
+
* controller. Web and Desktop mount one; profiles without it keep the
|
|
93
|
+
* `target-not-live` refusal.
|
|
94
|
+
* @param targetId - persisted session identity to resume.
|
|
95
|
+
* @returns the resumed live Agent.
|
|
96
|
+
* @throws {MessagingError} with `target-not-live` without a controller, or
|
|
97
|
+
* `cold-delivery-failed` when resume refuses.
|
|
98
|
+
*/
|
|
99
|
+
private resumeColdAgent;
|
|
100
|
+
/**
|
|
101
|
+
* Relay one message to a live peer session, resuming a persisted session
|
|
102
|
+
* first when `allowColdDelivery` is enabled.
|
|
84
103
|
*
|
|
85
104
|
* Success means the target agent accepted the message into its inbox and the
|
|
86
105
|
* session flush acknowledged persistence. It does not mean the target model
|
package/lib/service.js
CHANGED
|
@@ -95,7 +95,53 @@ export class SessionMessagingService extends Service {
|
|
|
95
95
|
throw new MessagingError('caller-not-top-level', `session ${caller.id} is not a user-launched top-level session; subagent sessions use the parent/child channel`);
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
98
|
+
* Enforce per-target consent and the sender→target rate limit once.
|
|
99
|
+
* @param caller - sending agent.
|
|
100
|
+
* @param targetId - target session identity.
|
|
101
|
+
*/
|
|
102
|
+
checkConsentAndRate(caller, targetId) {
|
|
103
|
+
const consent = this.consent.get(targetId);
|
|
104
|
+
if (this.config.requireTargetOptIn && consent?.enabled !== true) {
|
|
105
|
+
throw new MessagingError('target-not-opted-in', `session ${targetId} has not opted in to peer messages`);
|
|
106
|
+
}
|
|
107
|
+
if (consent?.enabled === false) {
|
|
108
|
+
throw new MessagingError('target-paused', `session ${targetId} has paused peer messages`);
|
|
109
|
+
}
|
|
110
|
+
const pairKey = `${caller.id}->${targetId}`;
|
|
111
|
+
if (!this.limiter.allow(pairKey, Date.now())) {
|
|
112
|
+
throw new MessagingError('rate-limited', `more than ${this.config.maxMessagesPerMinute} messages per minute were sent to this session`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resume a persisted, non-live session through the deployment's session
|
|
117
|
+
* controller. Web and Desktop mount one; profiles without it keep the
|
|
118
|
+
* `target-not-live` refusal.
|
|
119
|
+
* @param targetId - persisted session identity to resume.
|
|
120
|
+
* @returns the resumed live Agent.
|
|
121
|
+
* @throws {MessagingError} with `target-not-live` without a controller, or
|
|
122
|
+
* `cold-delivery-failed` when resume refuses.
|
|
123
|
+
*/
|
|
124
|
+
async resumeColdAgent(targetId) {
|
|
125
|
+
const controller = this.ctx.get('sessionController');
|
|
126
|
+
if (controller === undefined) {
|
|
127
|
+
throw new MessagingError('target-not-live', `session ${targetId} is not live and this profile has no session controller for cold resume`);
|
|
128
|
+
}
|
|
129
|
+
let resolved;
|
|
130
|
+
try {
|
|
131
|
+
resolved = await controller.resolveAgent(targetId);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw new MessagingError('cold-delivery-failed', `cold resume failed for ${targetId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
135
|
+
}
|
|
136
|
+
if ('error' in resolved) {
|
|
137
|
+
const detail = resolved.error.message === undefined ? '' : ` (${resolved.error.message})`;
|
|
138
|
+
throw new MessagingError('cold-delivery-failed', `cold resume failed for ${targetId}: ${resolved.error.code ?? 'error'}${detail}`);
|
|
139
|
+
}
|
|
140
|
+
return resolved.agent;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Relay one message to a live peer session, resuming a persisted session
|
|
144
|
+
* first when `allowColdDelivery` is enabled.
|
|
99
145
|
*
|
|
100
146
|
* Success means the target agent accepted the message into its inbox and the
|
|
101
147
|
* session flush acknowledged persistence. It does not mean the target model
|
|
@@ -130,9 +176,18 @@ export class SessionMessagingService extends Service {
|
|
|
130
176
|
.filter(([sessionId]) => sessionId !== caller.id)
|
|
131
177
|
.map(([sessionId, entry]) => ({ sessionId, scopes: entry.writeScopes }))
|
|
132
178
|
.filter(peer => peer.scopes.length > 0));
|
|
133
|
-
|
|
179
|
+
let target = this.ctx.agents.get(targetId);
|
|
180
|
+
let delivery = 'live';
|
|
181
|
+
let policyChecked = false;
|
|
134
182
|
if (target === undefined) {
|
|
135
|
-
|
|
183
|
+
if (!this.config.allowColdDelivery) {
|
|
184
|
+
throw new MessagingError('target-not-live', `session ${targetId} is not live and cold-session delivery is disabled`);
|
|
185
|
+
}
|
|
186
|
+
// Check consent and rate limits before paying for a resume/turn.
|
|
187
|
+
this.checkConsentAndRate(caller, targetId);
|
|
188
|
+
policyChecked = true;
|
|
189
|
+
target = await this.resumeColdAgent(targetId);
|
|
190
|
+
delivery = 'resumed';
|
|
136
191
|
}
|
|
137
192
|
if (!isTopLevelSessionHeader(target.session.header)) {
|
|
138
193
|
throw new MessagingError('target-not-top-level', `session ${targetId} is not a user-launched top-level session`);
|
|
@@ -140,17 +195,8 @@ export class SessionMessagingService extends Service {
|
|
|
140
195
|
if (this.config.sameDirectoryOnly.get() && !sameDirectory(caller.session.header.cwd, target.session.header.cwd)) {
|
|
141
196
|
throw new MessagingError('cross-directory', 'the target session works in a different directory');
|
|
142
197
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
throw new MessagingError('target-not-opted-in', `session ${targetId} has not opted in to peer messages`);
|
|
146
|
-
}
|
|
147
|
-
if (consent?.enabled === false) {
|
|
148
|
-
throw new MessagingError('target-paused', `session ${targetId} has paused peer messages`);
|
|
149
|
-
}
|
|
150
|
-
const pairKey = `${caller.id}->${targetId}`;
|
|
151
|
-
if (!this.limiter.allow(pairKey, Date.now())) {
|
|
152
|
-
throw new MessagingError('rate-limited', `more than ${this.config.maxMessagesPerMinute} messages per minute were sent to this session`);
|
|
153
|
-
}
|
|
198
|
+
if (!policyChecked)
|
|
199
|
+
this.checkConsentAndRate(caller, targetId);
|
|
154
200
|
const messageId = `session-message-${randomUUID()}`;
|
|
155
201
|
const text = buildRelayText({
|
|
156
202
|
senderSessionId: caller.id,
|
|
@@ -182,6 +228,7 @@ export class SessionMessagingService extends Service {
|
|
|
182
228
|
targetSessionId: targetId,
|
|
183
229
|
targetStatus: target.status,
|
|
184
230
|
conflicts,
|
|
231
|
+
delivery,
|
|
185
232
|
};
|
|
186
233
|
}
|
|
187
234
|
}
|
package/lib/tools.js
CHANGED
|
@@ -89,6 +89,7 @@ export function apply(ctx) {
|
|
|
89
89
|
messageId: { type: 'string', required: true },
|
|
90
90
|
targetSessionId: { type: 'string', required: true },
|
|
91
91
|
targetStatus: { type: 'string', required: true, enum: ['idle', 'running'] },
|
|
92
|
+
delivery: { type: 'string', required: true, enum: ['live', 'resumed'] },
|
|
92
93
|
conflicts: {
|
|
93
94
|
type: 'array',
|
|
94
95
|
required: true,
|
|
@@ -108,9 +109,10 @@ export function apply(ctx) {
|
|
|
108
109
|
const warning = value.conflicts.length === 0
|
|
109
110
|
? ''
|
|
110
111
|
: ` Conflict warning: ${value.conflicts.map(conflict => `${conflict.sessionId} also declared ${conflict.scopes.join(', ')}`).join('; ')}. Coordinate before writing those paths.`;
|
|
112
|
+
const resumed = value.delivery === 'resumed' ? ' The target was a persisted session and was resumed for delivery.' : '';
|
|
111
113
|
return [{
|
|
112
114
|
type: 'text',
|
|
113
|
-
text: `Message ${value.messageId} accepted by session ${value.targetSessionId} (${value.targetStatus}). Acceptance means the message is in that session's inbox; it does not confirm the peer processed it.${warning}`,
|
|
115
|
+
text: `Message ${value.messageId} accepted by session ${value.targetSessionId} (${value.targetStatus}). Acceptance means the message is in that session's inbox; it does not confirm the peer processed it.${resumed}${warning}`,
|
|
114
116
|
}];
|
|
115
117
|
},
|
|
116
118
|
presentationMeta: (_args, value) => ({ targetSessionId: value.targetSessionId, messageId: value.messageId }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-messaging",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Peer messaging between user-launched sessions in one workspace: discover peers, relay coordination messages, pause reception, and get advisory write-scope conflict warnings.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"@deepseek-ai/dsh-llm": "^0.2.1-rc.2",
|
|
63
63
|
"@deepseek-ai/dsh-session": "^0.2.1-rc.2",
|
|
64
64
|
"@deepseek-ai/dsh-tools": "^0.2.1-rc.2",
|
|
65
|
-
"@deepseek-ai/schemastery": "^3.18.5"
|
|
65
|
+
"@deepseek-ai/schemastery": "^3.18.5",
|
|
66
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.2.1-rc.2"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"@deepseek-ai/cordis": "^4.0.5",
|
|
@@ -73,13 +74,14 @@
|
|
|
73
74
|
"@deepseek-ai/dsh-tools": "^0.2.1-rc.2",
|
|
74
75
|
"@deepseek-ai/schemastery": "^3.18.5",
|
|
75
76
|
"@types/node": "^24.0.0",
|
|
76
|
-
"typescript": "^6.0.3"
|
|
77
|
+
"typescript": "^6.0.3",
|
|
78
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.2.1-rc.2"
|
|
77
79
|
},
|
|
78
80
|
"scripts": {
|
|
79
81
|
"build": "tsc -p tsconfig.build.json",
|
|
80
82
|
"typecheck": "tsc -p tsconfig.json",
|
|
81
83
|
"test": "node --experimental-strip-types --test tests/protocol.test.ts",
|
|
82
|
-
"test:integration": "node --experimental-strip-types tests/integration-smoke.ts && node --experimental-strip-types tests/directory-policy-smoke.ts",
|
|
84
|
+
"test:integration": "node --experimental-strip-types tests/integration-smoke.ts && node --experimental-strip-types tests/directory-policy-smoke.ts && node --experimental-strip-types tests/cold-delivery-smoke.ts",
|
|
83
85
|
"test:model-e2e": "node --experimental-strip-types tests/model-e2e-smoke.ts"
|
|
84
86
|
},
|
|
85
87
|
"dsh": {
|