@nanhara/hara 0.121.0 → 0.122.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 +85 -0
- package/README.md +40 -6
- package/dist/agent/failover.js +1 -1
- package/dist/agent/loop.js +158 -21
- package/dist/agent/reminders.js +22 -7
- package/dist/agent/repeat-guard.js +26 -7
- package/dist/agent/structured.js +231 -0
- package/dist/agent/touched.js +20 -6
- package/dist/config.js +62 -26
- package/dist/cron/deliver.js +37 -3
- package/dist/feedback.js +5 -9
- package/dist/fs-read.js +106 -12
- package/dist/fs-write.js +242 -16
- package/dist/gateway/dingtalk.js +4 -1
- package/dist/gateway/discord.js +53 -18
- package/dist/gateway/feishu.js +158 -57
- package/dist/gateway/flows-pending.js +720 -0
- package/dist/gateway/flows.js +391 -16
- package/dist/gateway/matrix.js +80 -15
- package/dist/gateway/mattermost.js +44 -32
- package/dist/gateway/media.js +659 -0
- package/dist/gateway/serve.js +657 -162
- package/dist/gateway/sessions.js +475 -78
- package/dist/gateway/signal.js +27 -22
- package/dist/gateway/slack.js +26 -17
- package/dist/gateway/telegram.js +32 -18
- package/dist/gateway/wecom.js +32 -24
- package/dist/gateway/weixin.js +127 -49
- package/dist/hooks.js +32 -20
- package/dist/index.js +640 -219
- package/dist/org/projects.js +347 -0
- package/dist/org/roles.js +38 -11
- package/dist/security/secrets.js +150 -0
- package/dist/serve/server.js +772 -317
- package/dist/serve/sessions.js +112 -28
- package/dist/session/store.js +337 -44
- package/dist/tools/all.js +1 -0
- package/dist/tools/builtin.js +61 -23
- package/dist/tools/codebase.js +3 -1
- package/dist/tools/computer.js +98 -92
- package/dist/tools/edit.js +11 -8
- package/dist/tools/patch.js +230 -31
- package/dist/tools/search.js +482 -72
- package/dist/tools/task.js +453 -0
- package/dist/tools/todo.js +67 -16
- package/dist/tools/web.js +364 -64
- package/dist/tui/run.js +26 -23
- package/dist/undo.js +83 -7
- package/package.json +1 -1
package/dist/serve/sessions.js
CHANGED
|
@@ -14,6 +14,25 @@ export class SessionHub {
|
|
|
14
14
|
constructor(store = realStore) {
|
|
15
15
|
this.store = store;
|
|
16
16
|
}
|
|
17
|
+
/** Mutate an on-disk session under the same single-writer lock used by live sessions. The load happens
|
|
18
|
+
* only AFTER acquisition, so a writer that finished immediately before us cannot be overwritten by a
|
|
19
|
+
* stale pre-lock snapshot. */
|
|
20
|
+
mutateStored(id, mutate) {
|
|
21
|
+
const lock = this.store.acquire(id);
|
|
22
|
+
if (!lock.ok)
|
|
23
|
+
return false;
|
|
24
|
+
try {
|
|
25
|
+
const current = this.store.load(id);
|
|
26
|
+
if (!current)
|
|
27
|
+
return false;
|
|
28
|
+
mutate(current);
|
|
29
|
+
this.store.save(current.meta, current.history);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
this.store.release(id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
17
36
|
create(o) {
|
|
18
37
|
const meta = {
|
|
19
38
|
id: newSessionId(),
|
|
@@ -25,29 +44,66 @@ export class SessionHub {
|
|
|
25
44
|
updatedAt: "",
|
|
26
45
|
source: "interactive", // serve sessions are user-driven (desktop/IDE clients)
|
|
27
46
|
};
|
|
28
|
-
this.store.acquire(meta.id); // fresh
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
47
|
+
const lock = this.store.acquire(meta.id); // fresh UUID, but filesystem errors must still fail closed
|
|
48
|
+
if (!lock.ok)
|
|
49
|
+
throw new Error(`could not acquire session lock for ${meta.id}${lock.pid ? ` (held by pid ${lock.pid})` : ""}`);
|
|
50
|
+
const s = { meta, history: [], provider: o.provider, approval: o.approval, autoApprove: new Set(), stats: { input: 0, output: 0 }, projectContext: o.projectContext, busy: false, configuring: false, pendingProviderTurns: 0, abort: null };
|
|
51
|
+
try {
|
|
52
|
+
this.sessions.set(meta.id, s);
|
|
53
|
+
this.store.save(meta, []); // an empty newly-created thread must survive restart and appear in lists
|
|
54
|
+
return s;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.sessions.delete(meta.id);
|
|
58
|
+
this.store.release(meta.id);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
32
61
|
}
|
|
33
62
|
/** Resume a persisted session. Returns the live session, or a lock/missing failure. */
|
|
34
63
|
resume(id, o) {
|
|
35
64
|
const live = this.sessions.get(id);
|
|
65
|
+
if (live?.busy || live?.configuring)
|
|
66
|
+
return { busy: true };
|
|
36
67
|
if (live)
|
|
37
68
|
return { session: live }; // already attached to this server
|
|
38
|
-
const prior = this.store.load(id);
|
|
39
|
-
if (!prior)
|
|
40
|
-
return { missing: true };
|
|
41
69
|
const lock = this.store.acquire(id);
|
|
42
70
|
if (!lock.ok)
|
|
43
71
|
return { lockedBy: lock.pid ?? 0 };
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
72
|
+
let keepLock = false;
|
|
73
|
+
try {
|
|
74
|
+
const prior = this.store.load(id); // lock-before-load: this is the authoritative latest snapshot
|
|
75
|
+
if (!prior)
|
|
76
|
+
return { missing: true };
|
|
77
|
+
// Credential/provider routing is live, while the model remains the session's explicit pin.
|
|
78
|
+
prior.meta.provider = o.provider.id;
|
|
79
|
+
const s = { meta: prior.meta, history: [...prior.history], provider: o.provider, approval: o.approval, autoApprove: new Set(), stats: { input: 0, output: 0 }, projectContext: o.projectContext, busy: false, configuring: false, pendingProviderTurns: 0, abort: null, effort: prior.meta.effort };
|
|
80
|
+
this.sessions.set(id, s);
|
|
81
|
+
keepLock = true; // live session owns it until delete/releaseAll
|
|
82
|
+
return { session: s };
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
if (!keepLock)
|
|
86
|
+
this.store.release(id);
|
|
87
|
+
}
|
|
47
88
|
}
|
|
48
89
|
get(id) {
|
|
49
90
|
return this.sessions.get(id);
|
|
50
91
|
}
|
|
92
|
+
/** Drop an attached but idle session and release its lock without deleting persistence. This is used
|
|
93
|
+
* when resume attached successfully but live provider validation failed before the client got a handle. */
|
|
94
|
+
detach(id) {
|
|
95
|
+
const live = this.sessions.get(id);
|
|
96
|
+
if (!live || live.busy || live.configuring)
|
|
97
|
+
return false;
|
|
98
|
+
this.sessions.delete(id);
|
|
99
|
+
this.store.release(id);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/** Read model/cwd routing metadata without attaching the session. The authoritative resume still reloads
|
|
103
|
+
* after acquiring its lock; callers use this only to build the likely provider before that handoff. */
|
|
104
|
+
peekMeta(id) {
|
|
105
|
+
return this.sessions.get(id)?.meta ?? this.store.load(id)?.meta;
|
|
106
|
+
}
|
|
51
107
|
list(cwd) {
|
|
52
108
|
return this.store.list(cwd);
|
|
53
109
|
}
|
|
@@ -64,36 +120,36 @@ export class SessionHub {
|
|
|
64
120
|
rename(id, title) {
|
|
65
121
|
const live = this.sessions.get(id);
|
|
66
122
|
if (live) {
|
|
123
|
+
if (live.busy || live.configuring)
|
|
124
|
+
return false;
|
|
67
125
|
live.meta.title = title;
|
|
68
126
|
this.store.save(live.meta, live.history);
|
|
69
127
|
return true;
|
|
70
128
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
prior.meta.title = title;
|
|
75
|
-
this.store.save(prior.meta, prior.history);
|
|
76
|
-
return true;
|
|
129
|
+
return this.mutateStored(id, (current) => {
|
|
130
|
+
current.meta.title = title;
|
|
131
|
+
});
|
|
77
132
|
}
|
|
78
133
|
/** Archive/unarchive (hidden from lists, kept on disk). Returns false when unknown. */
|
|
79
134
|
setArchived(id, on) {
|
|
80
135
|
const live = this.sessions.get(id);
|
|
81
136
|
if (live) {
|
|
137
|
+
if (live.busy || live.configuring)
|
|
138
|
+
return false;
|
|
82
139
|
live.meta.archived = on;
|
|
83
140
|
this.store.save(live.meta, live.history);
|
|
84
141
|
return true;
|
|
85
142
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
prior.meta.archived = on;
|
|
90
|
-
this.store.save(prior.meta, prior.history);
|
|
91
|
-
return true;
|
|
143
|
+
return this.mutateStored(id, (current) => {
|
|
144
|
+
current.meta.archived = on;
|
|
145
|
+
});
|
|
92
146
|
}
|
|
93
147
|
/** Fork: duplicate a session's history into a NEW session (codex thread/fork) — the non-destructive
|
|
94
148
|
* sibling of rewind. Source may be live or on-disk; the fork is always a fresh live session. */
|
|
95
149
|
fork(id, o) {
|
|
96
150
|
const live = this.sessions.get(id);
|
|
151
|
+
if (live?.busy || live?.configuring)
|
|
152
|
+
return { busy: true };
|
|
97
153
|
const src = live ?? this.store.load(id);
|
|
98
154
|
if (!src)
|
|
99
155
|
return { missing: true };
|
|
@@ -106,8 +162,13 @@ export class SessionHub {
|
|
|
106
162
|
createdAt: new Date().toISOString(),
|
|
107
163
|
updatedAt: "",
|
|
108
164
|
source: "interactive",
|
|
165
|
+
...(src.meta.workingSet ? { workingSet: [...src.meta.workingSet] } : {}),
|
|
166
|
+
...(src.meta.todos ? { todos: src.meta.todos.map((todo) => ({ ...todo, ...(todo.blockedBy ? { blockedBy: [...todo.blockedBy] } : {}) })) } : {}),
|
|
167
|
+
...(src.meta.effort ? { effort: src.meta.effort } : {}),
|
|
109
168
|
};
|
|
110
|
-
this.store.acquire(meta.id);
|
|
169
|
+
const lock = this.store.acquire(meta.id);
|
|
170
|
+
if (!lock.ok)
|
|
171
|
+
throw new Error(`could not acquire fork lock for ${meta.id}${lock.pid ? ` (held by pid ${lock.pid})` : ""}`);
|
|
111
172
|
const s = {
|
|
112
173
|
meta,
|
|
113
174
|
history: [...src.history],
|
|
@@ -117,20 +178,30 @@ export class SessionHub {
|
|
|
117
178
|
stats: { input: 0, output: 0 },
|
|
118
179
|
projectContext: o.projectContext,
|
|
119
180
|
busy: false,
|
|
181
|
+
configuring: false,
|
|
182
|
+
pendingProviderTurns: 0,
|
|
120
183
|
abort: null,
|
|
184
|
+
effort: src.meta.effort,
|
|
121
185
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
186
|
+
try {
|
|
187
|
+
this.sessions.set(meta.id, s);
|
|
188
|
+
this.store.save(meta, s.history); // persist immediately — a fork should survive a crash unsent
|
|
189
|
+
return { session: s };
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
this.sessions.delete(meta.id);
|
|
193
|
+
this.store.release(meta.id);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
125
196
|
}
|
|
126
197
|
/** Permanently delete (live or on-disk). Refuses a busy live session. Returns:
|
|
127
198
|
* "gone" on success, "busy" when a turn is running, "missing" when unknown/held elsewhere. */
|
|
128
199
|
delete(id) {
|
|
129
200
|
const live = this.sessions.get(id);
|
|
130
|
-
if (live?.busy)
|
|
201
|
+
if (live?.busy || live?.configuring)
|
|
131
202
|
return "busy";
|
|
132
203
|
const ok = this.store.delete(id);
|
|
133
|
-
if (!ok
|
|
204
|
+
if (!ok)
|
|
134
205
|
return "missing";
|
|
135
206
|
if (live)
|
|
136
207
|
this.sessions.delete(id);
|
|
@@ -143,4 +214,17 @@ export class SessionHub {
|
|
|
143
214
|
this.store.release(id);
|
|
144
215
|
this.sessions.clear();
|
|
145
216
|
}
|
|
217
|
+
/** Snapshot live sessions for graceful shutdown/health handling. */
|
|
218
|
+
active() {
|
|
219
|
+
return [...this.sessions.values()];
|
|
220
|
+
}
|
|
221
|
+
/** Release only idle sessions; logical work and abandoned-but-physical provider turns retain their locks. */
|
|
222
|
+
releaseIdle() {
|
|
223
|
+
for (const [id, session] of this.sessions) {
|
|
224
|
+
if (session.busy || session.configuring || session.pendingProviderTurns > 0)
|
|
225
|
+
continue;
|
|
226
|
+
this.store.release(id);
|
|
227
|
+
this.sessions.delete(id);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
146
230
|
}
|