@nanhara/hara 0.128.0 → 0.130.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 +40 -0
- package/README.md +7 -5
- package/dist/agent/loop.js +6 -6
- package/dist/agent/repeat-guard.js +33 -3
- package/dist/artifacts/store.js +292 -30
- package/dist/context/workspace-scope.js +23 -0
- package/dist/gateway/feishu.js +131 -30
- package/dist/index.js +61 -1
- package/dist/plugins/plugins.js +71 -2
- package/dist/security/private-state.js +19 -7
- package/dist/serve/protocol.js +6 -1
- package/dist/serve/server.js +89 -11
- package/dist/serve/task-events.js +10 -1
- package/package.json +1 -1
package/dist/serve/server.js
CHANGED
|
@@ -32,12 +32,12 @@ import { disposeTodoScope, onTodosChange, restoreTodos, serializeTodos } from ".
|
|
|
32
32
|
import { INTERJECT_PREFIX, disposeReminderScope } from "../agent/reminders.js";
|
|
33
33
|
import { SessionHub, realStore } from "./sessions.js";
|
|
34
34
|
import { parseFrame, rpcResult, rpcError, rpcNotify, ERR, PROTOCOL_VERSION } from "./protocol.js";
|
|
35
|
-
import { taskLifecycleEvent } from "./task-events.js";
|
|
35
|
+
import { taskLifecycleEvent, } from "./task-events.js";
|
|
36
36
|
import { readModelContextFileSync } from "../fs-read.js";
|
|
37
37
|
import { optionalPosixOpenFlag } from "../fs-open-flags.js";
|
|
38
38
|
import { sameOpenedFileIdentity } from "../fs-identity.js";
|
|
39
39
|
import { redactSensitiveText, redactSensitiveValue } from "../security/secrets.js";
|
|
40
|
-
import { ArtifactStoreError, getArtifact, importArtifact, listArtifactRevisions, listArtifacts, } from "../artifacts/store.js";
|
|
40
|
+
import { ArtifactStoreError, commitArtifact, getArtifact, importArtifact, listArtifactRevisions, listArtifacts, revertArtifact, } from "../artifacts/store.js";
|
|
41
41
|
import { consumePendingTaskSteering, createTaskExecution, continueTaskExecution, finishTaskExecution, newSteerInteraction, newTurnInteraction, recordTaskSteering, requestsTaskContinuation, taskExecutionContext, } from "../session/task.js";
|
|
42
42
|
const APPROVAL_TIMEOUT_MS = 300_000; // an unanswered approval denies after 5 min (never hangs a turn)
|
|
43
43
|
const COMPACT_TIMEOUT_MS = 60_000;
|
|
@@ -46,12 +46,18 @@ const SOCKET_CLOSE_GRACE_MS = 250;
|
|
|
46
46
|
const DISCOVERY_LOCK_WAIT_MS = 2_000;
|
|
47
47
|
const artifactRpcError = (id, error, action) => {
|
|
48
48
|
if (error instanceof ArtifactStoreError) {
|
|
49
|
-
const code = error.code === "ARTIFACT_CORRUPT"
|
|
49
|
+
const code = error.code === "ARTIFACT_CORRUPT"
|
|
50
|
+
? ERR.INTERNAL
|
|
51
|
+
: error.code === "ARTIFACT_CONFLICT"
|
|
52
|
+
? ERR.CONFLICT
|
|
53
|
+
: ERR.PARAMS;
|
|
50
54
|
return rpcError(id, code, error.message);
|
|
51
55
|
}
|
|
52
|
-
return rpcError(id, ERR.INTERNAL, action === "import"
|
|
53
|
-
?
|
|
54
|
-
:
|
|
56
|
+
return rpcError(id, ERR.INTERNAL, action === "import" || action === "commit"
|
|
57
|
+
? `Artifact ${action} failed safely; the source file was not modified`
|
|
58
|
+
: action === "revert"
|
|
59
|
+
? "Artifact revert failed safely; no current revision was replaced"
|
|
60
|
+
: `Artifact ${action} failed safely; local Artifact data was not changed`);
|
|
55
61
|
};
|
|
56
62
|
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
57
63
|
const isPidAlive = (pid) => {
|
|
@@ -289,6 +295,7 @@ export async function startServe(opts, deps) {
|
|
|
289
295
|
// Physical provider/tool work can outlive its logical timeout. Keep a process-level ledger independent
|
|
290
296
|
// of SessionHub membership so detach/delete cannot make an updater believe the old engine is quiescent.
|
|
291
297
|
const activeOperations = new Set();
|
|
298
|
+
let taskEventSequence = 0;
|
|
292
299
|
let closing = false;
|
|
293
300
|
let closePromise = null;
|
|
294
301
|
const trackActiveOperation = (operation) => {
|
|
@@ -352,10 +359,23 @@ export async function startServe(opts, deps) {
|
|
|
352
359
|
if (ws.readyState === ws.OPEN)
|
|
353
360
|
ws.send(frame);
|
|
354
361
|
};
|
|
362
|
+
const nextTaskEventCursor = () => {
|
|
363
|
+
const sequence = taskEventSequence + 1;
|
|
364
|
+
if (!Number.isSafeInteger(sequence)) {
|
|
365
|
+
throw new Error("task lifecycle event sequence exhausted");
|
|
366
|
+
}
|
|
367
|
+
return { streamId: instanceId, sequence };
|
|
368
|
+
};
|
|
369
|
+
const publishTaskState = (event) => {
|
|
370
|
+
// Commit the cursor immediately before the synchronous broadcast. Dedupe paths never consume one,
|
|
371
|
+
// while every published event has a unique position in this server-wide stream.
|
|
372
|
+
taskEventSequence = event.sequence;
|
|
373
|
+
broadcast("event.task_state", { ...event });
|
|
374
|
+
};
|
|
355
375
|
const broadcastTaskState = (session, activity) => {
|
|
356
376
|
if (!session.task)
|
|
357
377
|
return;
|
|
358
|
-
|
|
378
|
+
publishTaskState(taskLifecycleEvent(session.meta.id, session.task, session.meta.todos ?? [], activity, nextTaskEventCursor()));
|
|
359
379
|
};
|
|
360
380
|
// Discovery file — the desktop shell reads this to find the running server (like a pid/port file).
|
|
361
381
|
const discoveryDir = join(deps.discoveryHome ?? homedir(), ".hara");
|
|
@@ -442,13 +462,13 @@ export async function startServe(opts, deps) {
|
|
|
442
462
|
const emitTaskState = (activity, todos = s.meta.todos ?? []) => {
|
|
443
463
|
if (!s.task)
|
|
444
464
|
return;
|
|
445
|
-
const event = taskLifecycleEvent(sessionId, s.task, todos, activity);
|
|
446
|
-
const { at: _at, ...stableEvent } = event;
|
|
465
|
+
const event = taskLifecycleEvent(sessionId, s.task, todos, activity, nextTaskEventCursor());
|
|
466
|
+
const { at: _at, streamId: _streamId, sequence: _sequence, ...stableEvent } = event;
|
|
447
467
|
const signature = JSON.stringify(stableEvent);
|
|
448
468
|
if (signature === lastTaskStateSignature)
|
|
449
469
|
return;
|
|
450
470
|
lastTaskStateSignature = signature;
|
|
451
|
-
|
|
471
|
+
publishTaskState(event);
|
|
452
472
|
};
|
|
453
473
|
broadcast("event.turn_start", { sessionId, taskId: s.task.id, turnId: s.task.turnId });
|
|
454
474
|
emitTaskState({ state: "running", phase: "starting" });
|
|
@@ -742,7 +762,8 @@ export async function startServe(opts, deps) {
|
|
|
742
762
|
"approval.reply", "plugins.list", "plugins.set", "skills.list", "models.list", "files.search", "project.panels",
|
|
743
763
|
"settings.providers.list", "settings.providers.test", "settings.providers.save",
|
|
744
764
|
"automation.list", "automation.add", "automation.toggle", "automation.delete",
|
|
745
|
-
"artifact.import", "artifact.
|
|
765
|
+
"artifact.import", "artifact.commit", "artifact.revert",
|
|
766
|
+
"artifact.list", "artifact.get", "artifact.revisions",
|
|
746
767
|
"tasks.list", "approvals.list", "approvals.resolve",
|
|
747
768
|
];
|
|
748
769
|
const runtime = runtimeInfo();
|
|
@@ -1162,6 +1183,63 @@ export async function startServe(opts, deps) {
|
|
|
1162
1183
|
return reply(artifactRpcError(id, error, "import"));
|
|
1163
1184
|
}
|
|
1164
1185
|
}
|
|
1186
|
+
case "artifact.commit": {
|
|
1187
|
+
if (typeof p.artifactId !== "string"
|
|
1188
|
+
|| typeof p.baseRevisionId !== "string"
|
|
1189
|
+
|| typeof p.sourcePath !== "string"
|
|
1190
|
+
|| !p.sourcePath) {
|
|
1191
|
+
return reply(rpcError(id, ERR.PARAMS, "artifactId, baseRevisionId, and sourcePath required"));
|
|
1192
|
+
}
|
|
1193
|
+
if (p.actor !== undefined) {
|
|
1194
|
+
return reply(rpcError(id, ERR.PARAMS, "actor is assigned by the authenticated host"));
|
|
1195
|
+
}
|
|
1196
|
+
if (p.taskRunId !== undefined && typeof p.taskRunId !== "string") {
|
|
1197
|
+
return reply(rpcError(id, ERR.PARAMS, "taskRunId must be a string"));
|
|
1198
|
+
}
|
|
1199
|
+
if (p.changedPaths !== undefined && !Array.isArray(p.changedPaths)) {
|
|
1200
|
+
return reply(rpcError(id, ERR.PARAMS, "changedPaths must be an array"));
|
|
1201
|
+
}
|
|
1202
|
+
try {
|
|
1203
|
+
const details = await commitArtifact(artifactHome, {
|
|
1204
|
+
artifactId: p.artifactId,
|
|
1205
|
+
baseRevisionId: p.baseRevisionId,
|
|
1206
|
+
sourcePath: p.sourcePath,
|
|
1207
|
+
actor: "user",
|
|
1208
|
+
...(p.taskRunId !== undefined ? { taskRunId: p.taskRunId } : {}),
|
|
1209
|
+
...(p.changedPaths !== undefined ? { changedPaths: p.changedPaths } : {}),
|
|
1210
|
+
});
|
|
1211
|
+
return reply(rpcResult(id, details));
|
|
1212
|
+
}
|
|
1213
|
+
catch (error) {
|
|
1214
|
+
return reply(artifactRpcError(id, error, "commit"));
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
case "artifact.revert": {
|
|
1218
|
+
if (typeof p.artifactId !== "string"
|
|
1219
|
+
|| typeof p.baseRevisionId !== "string"
|
|
1220
|
+
|| typeof p.targetRevisionId !== "string") {
|
|
1221
|
+
return reply(rpcError(id, ERR.PARAMS, "artifactId, baseRevisionId, and targetRevisionId required"));
|
|
1222
|
+
}
|
|
1223
|
+
if (p.actor !== undefined) {
|
|
1224
|
+
return reply(rpcError(id, ERR.PARAMS, "actor is assigned by the authenticated host"));
|
|
1225
|
+
}
|
|
1226
|
+
if (p.taskRunId !== undefined && typeof p.taskRunId !== "string") {
|
|
1227
|
+
return reply(rpcError(id, ERR.PARAMS, "taskRunId must be a string"));
|
|
1228
|
+
}
|
|
1229
|
+
try {
|
|
1230
|
+
const details = revertArtifact(artifactHome, {
|
|
1231
|
+
artifactId: p.artifactId,
|
|
1232
|
+
baseRevisionId: p.baseRevisionId,
|
|
1233
|
+
targetRevisionId: p.targetRevisionId,
|
|
1234
|
+
actor: "user",
|
|
1235
|
+
...(p.taskRunId !== undefined ? { taskRunId: p.taskRunId } : {}),
|
|
1236
|
+
});
|
|
1237
|
+
return reply(rpcResult(id, details));
|
|
1238
|
+
}
|
|
1239
|
+
catch (error) {
|
|
1240
|
+
return reply(artifactRpcError(id, error, "revert"));
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1165
1243
|
case "artifact.list": {
|
|
1166
1244
|
try {
|
|
1167
1245
|
return reply(rpcResult(id, listArtifacts(artifactHome)));
|
|
@@ -5,7 +5,14 @@ function bounded(value, max) {
|
|
|
5
5
|
}
|
|
6
6
|
/** Build the single task-state shape consumed by Desktop, IDE clients, and the companion. Conversation
|
|
7
7
|
* streaming remains separate; this event is the authoritative execution/status plane. */
|
|
8
|
-
export function taskLifecycleEvent(sessionId, task, todos = [], activity, at = new Date().toISOString()) {
|
|
8
|
+
export function taskLifecycleEvent(sessionId, task, todos = [], activity, cursor, at = new Date().toISOString()) {
|
|
9
|
+
const streamId = cursor.streamId.trim();
|
|
10
|
+
if (!streamId || streamId.length > 128) {
|
|
11
|
+
throw new Error("task lifecycle streamId must contain 1-128 characters");
|
|
12
|
+
}
|
|
13
|
+
if (!Number.isSafeInteger(cursor.sequence) || cursor.sequence <= 0) {
|
|
14
|
+
throw new Error("task lifecycle sequence must be a positive safe integer");
|
|
15
|
+
}
|
|
9
16
|
const current = todos.find((todo) => todo.status === "in_progress")
|
|
10
17
|
?? todos.find((todo) => todo.status === "pending");
|
|
11
18
|
const detail = bounded(activity.detail, 500);
|
|
@@ -22,6 +29,8 @@ export function taskLifecycleEvent(sessionId, task, todos = [], activity, at = n
|
|
|
22
29
|
: task.status;
|
|
23
30
|
return {
|
|
24
31
|
version: TASK_LIFECYCLE_EVENT_VERSION,
|
|
32
|
+
streamId,
|
|
33
|
+
sequence: cursor.sequence,
|
|
25
34
|
sessionId,
|
|
26
35
|
taskId: task.id,
|
|
27
36
|
turnId: task.turnId,
|