dsh-autoresume 0.1.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/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # dsh-autoresume — dsh 会话重启自动续跑插件
2
+
3
+ web 进程重启后,若目标会话停在"被打断的中间态"(open turn / 无结果的 tool call / turn/end interrupted),
4
+ 自动向该会话注入一条「继续」,让 agent 接着干活——无需人工干预。
5
+
6
+ ## 特性
7
+
8
+ - 只服务**一个**配置的 `targetSessionId`,绝不扫描其他会话
9
+ - 通过 `ctx.sessionPersistence.inspect()` 读取会话持久化事件流做状态判定
10
+ - 判定逻辑:
11
+ - **interrupted**(注入「继续」):turn/step 打开未闭合、存在无对应 `tool/result` 的 `tool/call`、最后一个 `turn/end` 原因为 `interrupted`
12
+ - **completed**(不动作):最后是 `assistant/message`,或 `turn/end` 为 `completed` 且其后无新用户消息
13
+ - **settled**(不动作):`cancelled` / `error` 等已闭合终态;以及闭合轮次后出现的手动用户消息
14
+ - 注入方式:`ctx.agents.get(target)` → `agent.followup(userMessage)`(消息 `source.kind=plugin, form=notice`)
15
+ - **进程级一次性**:每个 web 进程只判定/注入一次;`bootGraceMs` 宽限窗口内有效(profile HMR 装卸插件不误触发)
16
+ - 目标会话未就绪时按 `pollIntervalMs` 重查
17
+
18
+ ## 安装
19
+
20
+ ```bash
21
+ # 1. 构建
22
+ npm run build
23
+
24
+ # 2. 注册到 web profile
25
+ dsh plugin --profile web add /path/to/dsh-autoresume
26
+
27
+ # 3. 配置目标会话(编辑 web profile 的 cordis.patch.yml)
28
+ - insert:
29
+ - id: dsh-autoresume
30
+ name: 'dsh-autoresume'
31
+ config:
32
+ targetSessionId: '<your-session-id>' # 必填
33
+
34
+ # 4. 验证注册
35
+ dsh --profile web --dump-config # 应出现 id: dsh-autoresume
36
+
37
+ # 5. 重启生效
38
+ sudo systemctl restart dsh-web
39
+ ```
40
+
41
+ ## 配置项
42
+
43
+ | 键 | 默认 | 说明 |
44
+ |---|---|---|
45
+ | `targetSessionId` | (必填) | 要自动续跑的目标会话 ID;未配置则插件不动作 |
46
+ | `bootGraceMs` | `120000` | web 进程启动后允许判定的宽限窗口(毫秒) |
47
+ | `initialDelayMs` | `3000` | 首次检查延迟(等会话恢复) |
48
+ | `pollIntervalMs` | `5000` | 会话未就绪时的重查间隔 |
49
+ | `promptText` | `继续(自动)` | 注入的续跑提示词 |
50
+
51
+ ## 工作原理
52
+
53
+ ```
54
+ web 重启 → 插件加载 → 宽限窗口内 inspect(目标会话)
55
+ ├─ interrupted → agent.followup('继续(自动)') → 自动接续
56
+ ├─ completed / settled → 不动作(避免重复注入)
57
+ └─ 会话未就绪 → 5s 后重查
58
+ ```
59
+
60
+ ## 架构
61
+
62
+ ```
63
+ src/
64
+ ├── service.js # 服务端:状态机判定 + 注入(构建到 lib/)
65
+ └── client.js # 客户端占位(构建到 lib/)
66
+ scripts/
67
+ ├── build.mjs # 构建:src → lib
68
+ └── self-test.mjs # 状态机单测(9 用例)
69
+ ```
70
+
71
+ ## 开发
72
+
73
+ ```bash
74
+ npm run build # src → lib
75
+ npm test # 状态机自测
76
+ ```
77
+
78
+ ## 故障排查
79
+
80
+ | 现象 | 原因 | 处理 |
81
+ |---|---|---|
82
+ | 插件不动作 | `targetSessionId` 未配置 | 检查 cordis.patch.yml 配置项 |
83
+ | 注入后 agent 不接续 | 会话处于 `running` 而非 `idle` | 插件只在 idle 时注入,等其空闲 |
84
+ | 重启后无「继续」 | 超过 `bootGraceMs` 窗口 | 增大窗口或确认在启动后 2 分钟内判定 |
85
+ | HMR 装卸后误触发 | —— | 已防:判定仅在新进程启动宽限内进行 |
86
+
87
+ ## 卸载
88
+
89
+ ```bash
90
+ dsh plugin --profile web remove dsh-autoresume
91
+ ```
92
+
93
+ ## License
94
+
95
+ Apache-2.0
@@ -0,0 +1,14 @@
1
+ # dsh-autoresume:web 启动后检测目标会话的持久化事件流,若被重启打断则注入一条「继续」。
2
+ # 单入口(包名即入口名,客户端由 package.json 的 dsh.client 声明扫描)。
3
+ - insert:
4
+ - id: dsh-autoresume
5
+ name: 'dsh-autoresume'
6
+ config:
7
+ # 必填:要自动续跑的目标会话 ID(填你自己的)
8
+ targetSessionId: '<your-session-id>'
9
+ # web 进程启动后允许判定的宽限窗口(毫秒)
10
+ bootGraceMs: 120000
11
+ initialDelayMs: 3000
12
+ pollIntervalMs: 5000
13
+ # 注入的续跑提示词
14
+ promptText: '继续(自动)'
package/lib/client.js ADDED
@@ -0,0 +1,11 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-autoresume", factory: (require) => { var module = { exports: {} }; var exports = module.exports;
2
+ // dsh-autoresume 浏览器端为无界面占位:全部逻辑在服务端入口。
3
+ // 保留 client 入口是为了与 dsh.client 声明一致(package.json exports["./client"])。
4
+ const name = 'dsh-autoresume/client';
5
+
6
+ function apply() {
7
+ if (typeof console !== 'undefined') console.debug('[dsh-autoresume] client no-op');
8
+ }
9
+ module.exports = { name, apply };
10
+
11
+ return module.exports; } });
package/lib/service.js ADDED
@@ -0,0 +1,224 @@
1
+ /**
2
+ * dsh-autoresume:web 启动后只对固定的开发会话做一次「重启打断检测」,
3
+ * 若持久化事件流停在 turn/start 或 tool/ 调用链中间态且没有 assistant 完成回复,
4
+ * 就通过 agent.followup() 注入一条「继续」。只服务 targetSessionId 一个会话。
5
+ */
6
+ import { randomUUID } from 'node:crypto';
7
+
8
+ export const name = 'dsh-autoresume';
9
+ export const inject = ['agents', 'sessions', 'sessionPersistence'];
10
+
11
+ const DEFAULT_TARGET_SESSION_ID = '';
12
+ const DEFAULT_PROMPT_TEXT = '继续(自动)';
13
+ const NOTICE_SUMMARY = 'autoresume:检测到上次重启打断,自动继续';
14
+
15
+ function positiveInt(value, fallback) {
16
+ const n = Number(value);
17
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
18
+ }
19
+
20
+ function toolResultCallId(data) {
21
+ const source = data?.message?.source;
22
+ if (source && source.kind === 'tool') return source.callId;
23
+ const blocks = data?.message?.content;
24
+ if (Array.isArray(blocks)) {
25
+ for (const block of blocks) {
26
+ if (block && typeof block === 'object' && block.callId !== undefined) return block.callId;
27
+ }
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ function isOurContinueMessage(event, continueText = DEFAULT_PROMPT_TEXT) {
33
+ const data = event?.data;
34
+ const source = data?.source;
35
+ if (!source || source.kind !== 'plugin' || source.plugin !== 'dsh-autoresume') return false;
36
+ const blocks = data?.content;
37
+ if (!Array.isArray(blocks)) return false;
38
+ return blocks.some(block => block && block.type === 'text' && block.text === continueText);
39
+ }
40
+
41
+ /**
42
+ * 对持久化事件流做一次性状态判定:
43
+ * - completed:最后是 assistant 完成回复,或最后一个 turn/end 已完成且其后没有新用户消息;
44
+ * - interrupted:turn/step 打开未闭合、存在无结果的 tool/call、或最后一个 turn/end 原因为 interrupted;
45
+ * - settled:有闭合边界但既非 completed 也非 interrupted(如 cancelled/error),不动作避免注入循环;
46
+ * - empty:尚无事件。
47
+ */
48
+ export function analyzeSessionEvents(events, continueText = DEFAULT_PROMPT_TEXT) {
49
+ const list = Array.isArray(events) ? events : [];
50
+ if (list.length === 0) return { state: 'empty', reason: 'no events' };
51
+
52
+ let lastTurnStart = -1;
53
+ let lastTurnEnd = -1;
54
+ let lastStepStart = -1;
55
+ let lastStepEnd = -1;
56
+ let lastAssistant = -1;
57
+ let lastUser = -1;
58
+ let lastUserIsOurs = false;
59
+ let lastTurnEndReason = null;
60
+ const pendingCalls = new Set();
61
+
62
+ for (const event of list) {
63
+ switch (event?.type) {
64
+ case 'turn/start':
65
+ lastTurnStart = event.seq;
66
+ break;
67
+ case 'turn/end':
68
+ lastTurnEnd = event.seq;
69
+ lastTurnEndReason = event.data?.reason?.kind ?? null;
70
+ break;
71
+ case 'step/start':
72
+ lastStepStart = event.seq;
73
+ break;
74
+ case 'step/end':
75
+ lastStepEnd = event.seq;
76
+ break;
77
+ case 'assistant/message':
78
+ lastAssistant = event.seq;
79
+ break;
80
+ case 'user/message':
81
+ lastUser = event.seq;
82
+ lastUserIsOurs = isOurContinueMessage(event, continueText);
83
+ break;
84
+ case 'tool/call': {
85
+ const callId = event.data?.callId;
86
+ if (callId !== undefined) pendingCalls.add(callId);
87
+ break;
88
+ }
89
+ case 'tool/result': {
90
+ const callId = toolResultCallId(event.data);
91
+ if (callId !== undefined) pendingCalls.delete(callId);
92
+ break;
93
+ }
94
+ default:
95
+ break;
96
+ }
97
+ }
98
+
99
+ const openTurn = lastTurnStart > lastTurnEnd;
100
+ const openStep = lastStepStart > lastStepEnd;
101
+ const pendingTool = pendingCalls.size > 0;
102
+ const lastEvent = list[list.length - 1];
103
+ const lastType = lastEvent?.type ?? null;
104
+
105
+ const reasons = [];
106
+ if (openTurn) reasons.push('open turn');
107
+ if (openStep) reasons.push('open step');
108
+ if (pendingTool) reasons.push(`${pendingCalls.size} pending tool call(s)`);
109
+
110
+ if (lastType === 'assistant/message') {
111
+ return { state: 'completed', reason: 'last event is assistant/message' };
112
+ }
113
+ if (reasons.length > 0) {
114
+ return { state: 'interrupted', reason: reasons.join(' + ') };
115
+ }
116
+ if (lastTurnEndReason === 'interrupted') {
117
+ return { state: 'interrupted', reason: 'last turn/end reason = interrupted' };
118
+ }
119
+ if (lastUser > lastTurnEnd) {
120
+ // 闭合轮次之后又出现一条用户消息:可能是我们的上次注入还没被处理,也可能是用户手动输入。
121
+ // 两种情况下都不再补发,避免同一会话堆积多条「继续」。
122
+ return {
123
+ state: lastUserIsOurs ? 'completed' : 'settled',
124
+ reason: lastUserIsOurs
125
+ ? 'previous autoresume prompt still pending'
126
+ : 'pending user message after completed turn; not touching'
127
+ };
128
+ }
129
+ if (lastTurnEndReason === 'completed') {
130
+ return { state: 'completed', reason: 'last turn/end reason = completed' };
131
+ }
132
+ return { state: 'settled', reason: `last turn/end reason = ${lastTurnEndReason ?? 'none'}; no mid-state` };
133
+ }
134
+
135
+ export function buildContinueMessage(promptText = DEFAULT_PROMPT_TEXT) {
136
+ return Object.freeze({
137
+ id: randomUUID(),
138
+ role: 'user',
139
+ content: Object.freeze([Object.freeze({ type: 'text', text: String(promptText) })]),
140
+ source: Object.freeze({ kind: 'plugin', plugin: 'dsh-autoresume', form: 'notice', summary: NOTICE_SUMMARY })
141
+ });
142
+ }
143
+
144
+ export function apply(ctx, config = {}) {
145
+ const targetSessionId = typeof config.targetSessionId === 'string' && config.targetSessionId !== ''
146
+ ? config.targetSessionId
147
+ : DEFAULT_TARGET_SESSION_ID;
148
+ if (targetSessionId === '') {
149
+ ctx.logger.warn('dsh-autoresume: no targetSessionId configured — plugin disarmed; set config.targetSessionId');
150
+ return;
151
+ }
152
+ const bootGraceMs = positiveInt(config.bootGraceMs, 120000);
153
+ const initialDelayMs = positiveInt(config.initialDelayMs, 3000);
154
+ const pollIntervalMs = positiveInt(config.pollIntervalMs, 5000);
155
+ const promptText = typeof config.promptText === 'string' && config.promptText !== ''
156
+ ? config.promptText
157
+ : DEFAULT_PROMPT_TEXT;
158
+
159
+ const bootStartedAtMs = Date.now() - Math.floor(process.uptime() * 1000);
160
+ let settled = false;
161
+ let injected = false;
162
+ let polls = 0;
163
+ let intervalHandle = null;
164
+
165
+ function finishDecision() {
166
+ settled = true;
167
+ if (intervalHandle !== null) {
168
+ clearInterval(intervalHandle);
169
+ intervalHandle = null;
170
+ }
171
+ }
172
+
173
+ async function checkOnce(source) {
174
+ if (settled || injected) return;
175
+ polls += 1;
176
+ // 只在 web 进程真正重启后的宽限窗口内行动;profile HMR 装/卸插件不会拿到新进程。
177
+ if (Date.now() - bootStartedAtMs > bootGraceMs) {
178
+ finishDecision();
179
+ ctx.logger.info(`dsh-autoresume: boot grace expired (${source}) — disarmed for this process`);
180
+ console.error(`[dsh-autoresume] boot grace expired (${source}) — disarmed for this process`);
181
+ return;
182
+ }
183
+ const agent = ctx.agents.get(targetSessionId);
184
+ if (agent === undefined) return; // 目标会话尚未恢复为 live,下一轮再查
185
+ if (agent.status !== 'idle') return; // 正在运行,绝不打断
186
+ console.error(`[dsh-autoresume] check ${source}: target agent live and idle`);
187
+ let inspection;
188
+ try {
189
+ inspection = await ctx.sessionPersistence.inspect(targetSessionId);
190
+ } catch (error) {
191
+ ctx.logger.warn(`dsh-autoresume: inspect(${targetSessionId}) failed: ${String(error?.message ?? error)}`);
192
+ return;
193
+ }
194
+ finishDecision(); // 每个进程只判定/注入一次
195
+ const analysis = analyzeSessionEvents(inspection.events, promptText);
196
+ ctx.logger.info(`dsh-autoresume: ${targetSessionId} state=${analysis.state} reason=${analysis.reason} source=${source}`);
197
+ console.error(`[dsh-autoresume] ${targetSessionId} state=${analysis.state} reason=${analysis.reason} source=${source}`);
198
+ if (analysis.state !== 'interrupted') return;
199
+ try {
200
+ agent.followup(buildContinueMessage(promptText));
201
+ injected = true;
202
+ ctx.logger.info(`dsh-autoresume: injected「${promptText}」into ${targetSessionId}`);
203
+ } catch (error) {
204
+ ctx.logger.warn(`dsh-autoresume: inject into ${targetSessionId} failed: ${String(error?.message ?? error)}`);
205
+ }
206
+ }
207
+
208
+ ctx.effect(() => {
209
+ const initial = setTimeout(() => { void checkOnce('initial'); }, initialDelayMs);
210
+ intervalHandle = setInterval(() => { void checkOnce('poll'); }, pollIntervalMs);
211
+ const off = ctx.on('agent/session-start', (payload) => {
212
+ const id = payload?.agent?.session?.id ?? payload?.agent?.id;
213
+ if (id === targetSessionId) {
214
+ console.error('[dsh-autoresume] agent/session-start observed for target');
215
+ void checkOnce('agent/session-start');
216
+ }
217
+ });
218
+ return () => {
219
+ clearTimeout(initial);
220
+ if (intervalHandle !== null) clearInterval(intervalHandle);
221
+ off();
222
+ };
223
+ }, 'dsh-autoresume: boot-time watcher');
224
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "dsh-autoresume",
3
+ "version": "0.1.0",
4
+ "description": "dsh web 重启后自动向固定开发会话注入「继续」:只服务目标会话,完成态不动作,避免重复注入。",
5
+ "type": "module",
6
+ "main": "./lib/service.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./lib/service.js"
10
+ },
11
+ "./client": {
12
+ "default": "./lib/client.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "cordis.patch.yml",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "keywords": [
23
+ "dsh",
24
+ "dsh-plugin",
25
+ "deepseek-harness",
26
+ "automation",
27
+ "session",
28
+ "resume",
29
+ "auto-continue"
30
+ ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/shengyvself/dsh-autoresume.git"
34
+ },
35
+ "homepage": "https://github.com/shengyvself/dsh-autoresume#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/shengyvself/dsh-autoresume/issues"
38
+ },
39
+ "engines": {
40
+ "node": ">=20"
41
+ },
42
+ "dsh": {
43
+ "bundle": {
44
+ "patch": "./cordis.patch.yml"
45
+ },
46
+ "client": {
47
+ "inject": [
48
+ "@deepseek-ai/dsh-client-runtime"
49
+ ],
50
+ "platform": "web"
51
+ }
52
+ },
53
+ "scripts": {
54
+ "build": "node scripts/build.mjs",
55
+ "test": "node scripts/self-test.mjs"
56
+ },
57
+ "peerDependencies": {
58
+ "@deepseek-ai/cordis": ">=4.0.1",
59
+ "@deepseek-ai/dsh-agent": ">=0.1.0-rc.5",
60
+ "@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.5",
61
+ "@deepseek-ai/dsh-session": ">=0.1.0-rc.5",
62
+ "@deepseek-ai/dsh-session-persistence": ">=0.1.0-rc.5"
63
+ },
64
+ "license": "Apache-2.0"
65
+ }