@nanhara/hara 0.121.1 → 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 +66 -0
- package/README.md +40 -6
- package/dist/agent/loop.js +136 -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 +33 -23
- package/dist/cron/deliver.js +37 -3
- package/dist/feedback.js +3 -2
- 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 +631 -222
- package/dist/org/projects.js +347 -0
- package/dist/org/roles.js +38 -11
- package/dist/security/secrets.js +84 -9
- package/dist/serve/server.js +772 -317
- package/dist/serve/sessions.js +113 -33
- package/dist/session/store.js +298 -47
- package/dist/tools/all.js +1 -0
- package/dist/tools/builtin.js +30 -28
- 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 +168 -54
- 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,33 +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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
}
|
|
51
88
|
}
|
|
52
89
|
get(id) {
|
|
53
90
|
return this.sessions.get(id);
|
|
54
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
|
+
}
|
|
55
107
|
list(cwd) {
|
|
56
108
|
return this.store.list(cwd);
|
|
57
109
|
}
|
|
@@ -68,36 +120,36 @@ export class SessionHub {
|
|
|
68
120
|
rename(id, title) {
|
|
69
121
|
const live = this.sessions.get(id);
|
|
70
122
|
if (live) {
|
|
123
|
+
if (live.busy || live.configuring)
|
|
124
|
+
return false;
|
|
71
125
|
live.meta.title = title;
|
|
72
126
|
this.store.save(live.meta, live.history);
|
|
73
127
|
return true;
|
|
74
128
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
prior.meta.title = title;
|
|
79
|
-
this.store.save(prior.meta, prior.history);
|
|
80
|
-
return true;
|
|
129
|
+
return this.mutateStored(id, (current) => {
|
|
130
|
+
current.meta.title = title;
|
|
131
|
+
});
|
|
81
132
|
}
|
|
82
133
|
/** Archive/unarchive (hidden from lists, kept on disk). Returns false when unknown. */
|
|
83
134
|
setArchived(id, on) {
|
|
84
135
|
const live = this.sessions.get(id);
|
|
85
136
|
if (live) {
|
|
137
|
+
if (live.busy || live.configuring)
|
|
138
|
+
return false;
|
|
86
139
|
live.meta.archived = on;
|
|
87
140
|
this.store.save(live.meta, live.history);
|
|
88
141
|
return true;
|
|
89
142
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
prior.meta.archived = on;
|
|
94
|
-
this.store.save(prior.meta, prior.history);
|
|
95
|
-
return true;
|
|
143
|
+
return this.mutateStored(id, (current) => {
|
|
144
|
+
current.meta.archived = on;
|
|
145
|
+
});
|
|
96
146
|
}
|
|
97
147
|
/** Fork: duplicate a session's history into a NEW session (codex thread/fork) — the non-destructive
|
|
98
148
|
* sibling of rewind. Source may be live or on-disk; the fork is always a fresh live session. */
|
|
99
149
|
fork(id, o) {
|
|
100
150
|
const live = this.sessions.get(id);
|
|
151
|
+
if (live?.busy || live?.configuring)
|
|
152
|
+
return { busy: true };
|
|
101
153
|
const src = live ?? this.store.load(id);
|
|
102
154
|
if (!src)
|
|
103
155
|
return { missing: true };
|
|
@@ -105,13 +157,18 @@ export class SessionHub {
|
|
|
105
157
|
id: newSessionId(),
|
|
106
158
|
cwd: src.meta.cwd,
|
|
107
159
|
provider: o.providerId,
|
|
108
|
-
model:
|
|
160
|
+
model: src.meta.model,
|
|
109
161
|
title: src.meta.title ? `${src.meta.title} ⑂` : "",
|
|
110
162
|
createdAt: new Date().toISOString(),
|
|
111
163
|
updatedAt: "",
|
|
112
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 } : {}),
|
|
113
168
|
};
|
|
114
|
-
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})` : ""}`);
|
|
115
172
|
const s = {
|
|
116
173
|
meta,
|
|
117
174
|
history: [...src.history],
|
|
@@ -121,20 +178,30 @@ export class SessionHub {
|
|
|
121
178
|
stats: { input: 0, output: 0 },
|
|
122
179
|
projectContext: o.projectContext,
|
|
123
180
|
busy: false,
|
|
181
|
+
configuring: false,
|
|
182
|
+
pendingProviderTurns: 0,
|
|
124
183
|
abort: null,
|
|
184
|
+
effort: src.meta.effort,
|
|
125
185
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
}
|
|
129
196
|
}
|
|
130
197
|
/** Permanently delete (live or on-disk). Refuses a busy live session. Returns:
|
|
131
198
|
* "gone" on success, "busy" when a turn is running, "missing" when unknown/held elsewhere. */
|
|
132
199
|
delete(id) {
|
|
133
200
|
const live = this.sessions.get(id);
|
|
134
|
-
if (live?.busy)
|
|
201
|
+
if (live?.busy || live?.configuring)
|
|
135
202
|
return "busy";
|
|
136
203
|
const ok = this.store.delete(id);
|
|
137
|
-
if (!ok
|
|
204
|
+
if (!ok)
|
|
138
205
|
return "missing";
|
|
139
206
|
if (live)
|
|
140
207
|
this.sessions.delete(id);
|
|
@@ -147,4 +214,17 @@ export class SessionHub {
|
|
|
147
214
|
this.store.release(id);
|
|
148
215
|
this.sessions.clear();
|
|
149
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
|
+
}
|
|
150
230
|
}
|