sentinelayer-cli 0.6.2 → 0.8.1
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/README.md +1009 -996
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +64 -63
- package/src/agents/ai-governance/index.js +12 -0
- package/src/agents/ai-governance/tools/base.js +171 -0
- package/src/agents/ai-governance/tools/eval-regression.js +47 -0
- package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
- package/src/agents/ai-governance/tools/index.js +52 -0
- package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
- package/src/agents/ai-governance/tools/provenance-check.js +69 -0
- package/src/agents/backend/index.js +12 -0
- package/src/agents/backend/tools/base.js +189 -0
- package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
- package/src/agents/backend/tools/idempotency-audit.js +105 -0
- package/src/agents/backend/tools/index.js +87 -0
- package/src/agents/backend/tools/retry-audit.js +132 -0
- package/src/agents/backend/tools/timeout-audit.js +144 -0
- package/src/agents/code-quality/index.js +12 -0
- package/src/agents/code-quality/tools/base.js +159 -0
- package/src/agents/code-quality/tools/complexity-measure.js +197 -0
- package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
- package/src/agents/code-quality/tools/cycle-detect.js +49 -0
- package/src/agents/code-quality/tools/dep-graph.js +196 -0
- package/src/agents/code-quality/tools/index.js +89 -0
- package/src/agents/data-layer/index.js +12 -0
- package/src/agents/data-layer/tools/base.js +181 -0
- package/src/agents/data-layer/tools/index-audit.js +165 -0
- package/src/agents/data-layer/tools/index.js +83 -0
- package/src/agents/data-layer/tools/migration-scan.js +135 -0
- package/src/agents/data-layer/tools/query-explain.js +120 -0
- package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
- package/src/agents/documentation/index.js +12 -0
- package/src/agents/documentation/tools/api-diff.js +91 -0
- package/src/agents/documentation/tools/base.js +151 -0
- package/src/agents/documentation/tools/dead-link-check.js +58 -0
- package/src/agents/documentation/tools/docstring-coverage.js +78 -0
- package/src/agents/documentation/tools/index.js +52 -0
- package/src/agents/documentation/tools/readme-freshness.js +61 -0
- package/src/agents/envelope/fix-cycle.js +45 -0
- package/src/agents/envelope/index.js +31 -0
- package/src/agents/envelope/loop.js +150 -0
- package/src/agents/envelope/pulse.js +18 -0
- package/src/agents/envelope/stream.js +40 -0
- package/src/agents/infrastructure/index.js +12 -0
- package/src/agents/infrastructure/tools/base.js +171 -0
- package/src/agents/infrastructure/tools/checkov-run.js +32 -0
- package/src/agents/infrastructure/tools/drift-detect.js +59 -0
- package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
- package/src/agents/infrastructure/tools/index.js +52 -0
- package/src/agents/infrastructure/tools/tflint-run.js +31 -0
- package/src/agents/jules/config/definition.js +160 -160
- package/src/agents/jules/config/system-prompt.js +182 -182
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +17 -17
- package/src/agents/jules/loop.js +460 -450
- package/src/agents/jules/pulse.js +10 -10
- package/src/agents/jules/stream.js +187 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +315 -309
- package/src/agents/jules/tools/aidenid-email.js +189 -189
- package/src/agents/jules/tools/auth-audit.js +1708 -1691
- package/src/agents/jules/tools/dispatch.js +340 -335
- package/src/agents/jules/tools/file-edit.js +2 -2
- package/src/agents/jules/tools/file-read.js +2 -2
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +2 -2
- package/src/agents/jules/tools/grep.js +2 -2
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +2 -2
- package/src/agents/jules/tools/runtime-audit.js +507 -507
- package/src/agents/jules/tools/shell.js +2 -2
- package/src/agents/jules/tools/url-policy.js +100 -100
- package/src/agents/mode.js +113 -0
- package/src/agents/observability/index.js +12 -0
- package/src/agents/observability/tools/alert-audit.js +39 -0
- package/src/agents/observability/tools/base.js +181 -0
- package/src/agents/observability/tools/dashboard-gap.js +42 -0
- package/src/agents/observability/tools/index.js +54 -0
- package/src/agents/observability/tools/log-schema-check.js +74 -0
- package/src/agents/observability/tools/span-coverage.js +74 -0
- package/src/agents/persona-visuals.js +102 -61
- package/src/agents/release/index.js +12 -0
- package/src/agents/release/tools/base.js +181 -0
- package/src/agents/release/tools/changelog-diff.js +86 -0
- package/src/agents/release/tools/feature-flag-audit.js +126 -0
- package/src/agents/release/tools/index.js +61 -0
- package/src/agents/release/tools/rollback-verify.js +129 -0
- package/src/agents/release/tools/semver-check.js +109 -0
- package/src/agents/reliability/index.js +12 -0
- package/src/agents/reliability/tools/backpressure-check.js +129 -0
- package/src/agents/reliability/tools/base.js +181 -0
- package/src/agents/reliability/tools/chaos-probe.js +109 -0
- package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
- package/src/agents/reliability/tools/health-check-audit.js +111 -0
- package/src/agents/reliability/tools/index.js +87 -0
- package/src/agents/run-persona.js +109 -0
- package/src/agents/security/index.js +12 -0
- package/src/agents/security/tools/authz-audit.js +134 -0
- package/src/agents/security/tools/base.js +190 -0
- package/src/agents/security/tools/crypto-review.js +175 -0
- package/src/agents/security/tools/index.js +97 -0
- package/src/agents/security/tools/sast-scan.js +175 -0
- package/src/agents/security/tools/secrets-scan.js +216 -0
- package/src/agents/shared-tools/dispatch-core.js +320 -315
- package/src/agents/shared-tools/file-edit.js +180 -180
- package/src/agents/shared-tools/file-read.js +100 -100
- package/src/agents/shared-tools/glob.js +168 -168
- package/src/agents/shared-tools/grep.js +228 -228
- package/src/agents/shared-tools/index.js +46 -46
- package/src/agents/shared-tools/path-guards.js +161 -161
- package/src/agents/shared-tools/shell.js +383 -383
- package/src/agents/supply-chain/index.js +12 -0
- package/src/agents/supply-chain/tools/attestation-check.js +42 -0
- package/src/agents/supply-chain/tools/base.js +151 -0
- package/src/agents/supply-chain/tools/index.js +52 -0
- package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
- package/src/agents/supply-chain/tools/package-verify.js +56 -0
- package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
- package/src/agents/testing/index.js +12 -0
- package/src/agents/testing/tools/base.js +202 -0
- package/src/agents/testing/tools/coverage-gap.js +144 -0
- package/src/agents/testing/tools/flake-detect.js +125 -0
- package/src/agents/testing/tools/index.js +85 -0
- package/src/agents/testing/tools/mutation-test.js +143 -0
- package/src/agents/testing/tools/snapshot-diff.js +103 -0
- package/src/ai/aidenid.js +1021 -1009
- package/src/ai/client.js +553 -553
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/proxy.js +137 -137
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/gate.js +428 -371
- package/src/auth/http.js +681 -611
- package/src/auth/service.js +1106 -1106
- package/src/auth/session-store.js +813 -813
- package/src/cli.js +257 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1338
- package/src/commands/ai/provision-governance.js +1272 -1272
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1171 -1166
- package/src/commands/auth.js +419 -419
- package/src/commands/chat.js +184 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +20 -10
- package/src/commands/mcp.js +461 -461
- package/src/commands/omargate.js +63 -29
- package/src/commands/persona.js +65 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +865 -872
- package/src/commands/session.js +1238 -0
- package/src/commands/spec.js +771 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +511 -511
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- package/src/coord/events-log.js +141 -0
- package/src/coord/handshake.js +719 -0
- package/src/coord/index.js +35 -0
- package/src/coord/paths.js +84 -0
- package/src/coord/priority.js +62 -0
- package/src/coord/tarjan.js +157 -0
- package/src/cost/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tokenizer.js +160 -0
- package/src/cost/tracker.js +232 -171
- package/src/daemon/artifact-lineage.js +896 -534
- package/src/daemon/assignment-ledger.js +1083 -770
- package/src/daemon/ast-drift.js +496 -0
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +1209 -626
- package/src/daemon/fix-cycle.js +384 -377
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/ingest-refresh.js +79 -11
- package/src/daemon/jira-lifecycle.js +767 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/pulse.js +327 -327
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/scope-engine.js +1068 -0
- package/src/daemon/watchdog.js +971 -971
- package/src/events/schema.js +190 -0
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +933 -918
- package/src/ingest/ownership.js +380 -0
- package/src/interactive/index.js +97 -97
- package/src/legacy-cli.js +3228 -2994
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/orchestrator/kai-chen.js +126 -0
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +136 -118
- package/src/review/ai-review.js +672 -679
- package/src/review/compliance-pack.js +389 -0
- package/src/review/investor-dd-config.js +54 -0
- package/src/review/investor-dd-file-loop.js +303 -0
- package/src/review/investor-dd-file-router.js +406 -0
- package/src/review/investor-dd-html-report.js +233 -0
- package/src/review/investor-dd-notification.js +120 -0
- package/src/review/investor-dd-orchestrator.js +405 -0
- package/src/review/investor-dd-persona-runner.js +275 -0
- package/src/review/live-validator.js +253 -0
- package/src/review/local-review.js +1351 -1305
- package/src/review/omargate-interactive.js +68 -68
- package/src/review/omargate-orchestrator.js +492 -300
- package/src/review/persona-prompts.js +484 -296
- package/src/review/reconciliation-rules.js +329 -0
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/reproducibility-chain.js +136 -0
- package/src/review/scan-modes.js +147 -42
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -67
- package/src/scaffold/templates.js +150 -150
- package/src/scan/generator.js +418 -418
- package/src/scan/gh-secrets.js +107 -107
- package/src/session/agent-registry.js +359 -0
- package/src/session/analytics.js +479 -0
- package/src/session/daemon.js +1396 -0
- package/src/session/file-locks.js +666 -0
- package/src/session/paths.js +37 -0
- package/src/session/recap.js +567 -0
- package/src/session/redact.js +82 -0
- package/src/session/runtime-bridge.js +762 -0
- package/src/session/scoring.js +406 -0
- package/src/session/setup-guides.js +304 -0
- package/src/session/store.js +704 -0
- package/src/session/stream.js +333 -0
- package/src/session/sync.js +753 -0
- package/src/session/tasks.js +1054 -0
- package/src/session/templates.js +188 -0
- package/src/spec/generator.js +619 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +569 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/session-tracker.js +234 -234
- package/src/telemetry/sync.js +203 -203
- package/src/ui/command-hints.js +13 -13
- package/src/ui/markdown.js +220 -220
|
@@ -0,0 +1,1396 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
import { invokeViaProxy } from "../ai/proxy.js";
|
|
6
|
+
import { createAgentEvent } from "../events/schema.js";
|
|
7
|
+
import {
|
|
8
|
+
buildDocumentsFromBlackboardEntries,
|
|
9
|
+
buildLocalHybridIndex,
|
|
10
|
+
buildSharedMemoryCorpus,
|
|
11
|
+
queryLocalHybridIndex,
|
|
12
|
+
} from "../memory/retrieval.js";
|
|
13
|
+
import {
|
|
14
|
+
endSession as endTelemetrySession,
|
|
15
|
+
recordLlmUsage,
|
|
16
|
+
startSession as startTelemetrySession,
|
|
17
|
+
} from "../telemetry/session-tracker.js";
|
|
18
|
+
import {
|
|
19
|
+
detectStaleAgents,
|
|
20
|
+
heartbeatAgent,
|
|
21
|
+
listAgents,
|
|
22
|
+
registerAgent,
|
|
23
|
+
unregisterAgent,
|
|
24
|
+
} from "./agent-registry.js";
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_FILE_LOCK_TTL_SECONDS,
|
|
27
|
+
lockFile,
|
|
28
|
+
unlockFile,
|
|
29
|
+
} from "./file-locks.js";
|
|
30
|
+
import { resolveSessionPaths } from "./paths.js";
|
|
31
|
+
import {
|
|
32
|
+
DEFAULT_RECAP_INACTIVITY_MS,
|
|
33
|
+
DEFAULT_RECAP_INTERVAL_MS,
|
|
34
|
+
emitPeriodicRecap,
|
|
35
|
+
} from "./recap.js";
|
|
36
|
+
import { stopRuntimeRunsForSession } from "./runtime-bridge.js";
|
|
37
|
+
import { pollHumanMessages } from "./sync.js";
|
|
38
|
+
import { getSession, renewSession } from "./store.js";
|
|
39
|
+
import { appendToStream, readStream, tailStream } from "./stream.js";
|
|
40
|
+
import { handleTaskDirective } from "./tasks.js";
|
|
41
|
+
|
|
42
|
+
const DAEMON_TICK_INTERVAL_MS = 30_000;
|
|
43
|
+
const HELP_REQUEST_TIMEOUT_MS = 1_200;
|
|
44
|
+
const HELP_MODEL_TIMEOUT_MS = 3_000;
|
|
45
|
+
const HELP_CONTEXT_EVENT_TAIL = 50;
|
|
46
|
+
const HELP_CONTEXT_RESULT_LIMIT = 6;
|
|
47
|
+
const HELP_BLACKBOARD_ENTRY_LIMIT = 40;
|
|
48
|
+
const FILE_CONFLICT_WINDOW_MS = 60_000;
|
|
49
|
+
const RENEWAL_WINDOW_MS = 60 * 60 * 1000;
|
|
50
|
+
const RENEWAL_THRESHOLD_EVENTS = 10;
|
|
51
|
+
const RENEWAL_LEAD_MS = 60 * 60 * 1000;
|
|
52
|
+
const DEFAULT_STALE_AGENT_SECONDS = 90;
|
|
53
|
+
const DEFAULT_RECAP_INTERVAL_MS_OVERRIDE = DEFAULT_RECAP_INTERVAL_MS;
|
|
54
|
+
const DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE = DEFAULT_RECAP_INACTIVITY_MS;
|
|
55
|
+
|
|
56
|
+
const SENTI_MODEL = "gpt-5.4-mini";
|
|
57
|
+
const SENTI_IDENTITY = Object.freeze({
|
|
58
|
+
id: "senti",
|
|
59
|
+
model: SENTI_MODEL,
|
|
60
|
+
persona: "Senti",
|
|
61
|
+
fullName: "Senti - SentinelLayer Session Daemon",
|
|
62
|
+
role: "daemon",
|
|
63
|
+
color: "magenta",
|
|
64
|
+
description:
|
|
65
|
+
"Session moderator, health monitor, and context provider. Short for SentinelLayer - your AI team lead.",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const ACTIVE_SENTI_DAEMONS = new Map();
|
|
69
|
+
|
|
70
|
+
function normalizeString(value) {
|
|
71
|
+
return String(value || "").trim();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeIsoTimestamp(value, fallbackIso = new Date().toISOString()) {
|
|
75
|
+
const normalized = normalizeString(value);
|
|
76
|
+
if (!normalized) {
|
|
77
|
+
return fallbackIso;
|
|
78
|
+
}
|
|
79
|
+
const epoch = Date.parse(normalized);
|
|
80
|
+
if (!Number.isFinite(epoch)) {
|
|
81
|
+
return fallbackIso;
|
|
82
|
+
}
|
|
83
|
+
return new Date(epoch).toISOString();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizePositiveInteger(value, fallbackValue) {
|
|
87
|
+
const normalized = Number(value);
|
|
88
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
89
|
+
return fallbackValue;
|
|
90
|
+
}
|
|
91
|
+
return Math.max(1, Math.floor(normalized));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildDaemonKey(sessionId, targetPath) {
|
|
95
|
+
return `${path.resolve(String(targetPath || "."))}::${normalizeString(sessionId)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function emitSentiEvent(
|
|
99
|
+
sessionId,
|
|
100
|
+
event,
|
|
101
|
+
payload = {},
|
|
102
|
+
{ targetPath = process.cwd(), nowIso = new Date().toISOString() } = {}
|
|
103
|
+
) {
|
|
104
|
+
const envelope = createAgentEvent({
|
|
105
|
+
event,
|
|
106
|
+
agentId: SENTI_IDENTITY.id,
|
|
107
|
+
agentModel: SENTI_IDENTITY.model,
|
|
108
|
+
sessionId,
|
|
109
|
+
ts: normalizeIsoTimestamp(nowIso, new Date().toISOString()),
|
|
110
|
+
payload,
|
|
111
|
+
});
|
|
112
|
+
await appendToStream(sessionId, envelope, {
|
|
113
|
+
targetPath,
|
|
114
|
+
});
|
|
115
|
+
return envelope;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatCodebaseSynopsis(session = {}) {
|
|
119
|
+
const summary = session.codebaseContext?.summary || {};
|
|
120
|
+
const loc = Number(summary.totalLoc || 0);
|
|
121
|
+
const files = Number(summary.filesScanned || 0);
|
|
122
|
+
const frameworks = Array.isArray(session.codebaseContext?.frameworks)
|
|
123
|
+
? session.codebaseContext.frameworks
|
|
124
|
+
: [];
|
|
125
|
+
const frameworkText = frameworks.length > 0 ? frameworks.slice(0, 3).join(", ") : "unknown stack";
|
|
126
|
+
return `${frameworkText}, ${files} files, ${loc.toLocaleString("en-US")} LOC`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildWelcomeMessage(session = {}, activeAgents = []) {
|
|
130
|
+
const roster = activeAgents
|
|
131
|
+
.filter((agent) => normalizeString(agent.agentId) !== SENTI_IDENTITY.id)
|
|
132
|
+
.map((agent) => `${agent.agentId} (${agent.status || "idle"})`)
|
|
133
|
+
.slice(0, 6);
|
|
134
|
+
const rosterText = roster.length > 0 ? roster.join(", ") : "no active agents yet";
|
|
135
|
+
return `Senti here. Session ${session.sessionId} is live. Codebase: ${formatCodebaseSynopsis(
|
|
136
|
+
session
|
|
137
|
+
)}. Active agents: ${rosterText}. Talk to me with @senti or /senti.`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function upsertSentiAgent(sessionId, { targetPath = process.cwd(), model = SENTI_MODEL } = {}) {
|
|
141
|
+
const activeAgents = await listAgents(sessionId, {
|
|
142
|
+
targetPath,
|
|
143
|
+
includeInactive: true,
|
|
144
|
+
});
|
|
145
|
+
const existing = activeAgents.find((agent) => normalizeString(agent.agentId) === SENTI_IDENTITY.id);
|
|
146
|
+
if (!existing) {
|
|
147
|
+
return registerAgent(sessionId, {
|
|
148
|
+
agentId: SENTI_IDENTITY.id,
|
|
149
|
+
model,
|
|
150
|
+
role: "daemon",
|
|
151
|
+
targetPath,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return heartbeatAgent(sessionId, SENTI_IDENTITY.id, {
|
|
155
|
+
status: "watching",
|
|
156
|
+
detail: "Monitoring session health and help requests.",
|
|
157
|
+
targetPath,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function createSentiState({
|
|
162
|
+
daemonKey,
|
|
163
|
+
sessionId,
|
|
164
|
+
targetPath,
|
|
165
|
+
startedAt,
|
|
166
|
+
model,
|
|
167
|
+
staleAgentSeconds,
|
|
168
|
+
helpRequestTimeoutMs,
|
|
169
|
+
tickIntervalMs,
|
|
170
|
+
recapIntervalMs,
|
|
171
|
+
recapInactivityMs,
|
|
172
|
+
helpResponder,
|
|
173
|
+
llmInvoker,
|
|
174
|
+
telemetrySessionId,
|
|
175
|
+
}) {
|
|
176
|
+
return {
|
|
177
|
+
daemonKey,
|
|
178
|
+
sessionId,
|
|
179
|
+
targetPath,
|
|
180
|
+
startedAt,
|
|
181
|
+
model,
|
|
182
|
+
staleAgentSeconds,
|
|
183
|
+
helpRequestTimeoutMs,
|
|
184
|
+
tickIntervalMs,
|
|
185
|
+
recapIntervalMs,
|
|
186
|
+
recapInactivityMs,
|
|
187
|
+
helpResponder,
|
|
188
|
+
llmInvoker,
|
|
189
|
+
telemetrySessionId,
|
|
190
|
+
running: true,
|
|
191
|
+
tickTimer: null,
|
|
192
|
+
helpAbortController: new AbortController(),
|
|
193
|
+
pendingHelpTimers: new Map(),
|
|
194
|
+
staleAlertedAgents: new Set(),
|
|
195
|
+
fileActivity: new Map(),
|
|
196
|
+
conflictAlertAt: new Map(),
|
|
197
|
+
lastTickAt: null,
|
|
198
|
+
lastTickSummary: null,
|
|
199
|
+
recapEmitter: null,
|
|
200
|
+
humanMessageCursor: null,
|
|
201
|
+
humanMessagePollInFlight: false,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function hasHelpResponseFromPeer(
|
|
206
|
+
sessionId,
|
|
207
|
+
requestEvent,
|
|
208
|
+
{
|
|
209
|
+
targetPath = process.cwd(),
|
|
210
|
+
} = {}
|
|
211
|
+
) {
|
|
212
|
+
const requester = normalizeString(requestEvent?.agent?.id);
|
|
213
|
+
const requestTs = normalizeIsoTimestamp(requestEvent?.ts, new Date().toISOString());
|
|
214
|
+
const events = await readStream(sessionId, {
|
|
215
|
+
targetPath,
|
|
216
|
+
tail: 0,
|
|
217
|
+
since: requestTs,
|
|
218
|
+
});
|
|
219
|
+
const requestEpoch = Date.parse(requestTs) || 0;
|
|
220
|
+
return events.some((event) => {
|
|
221
|
+
const eventAgentId = normalizeString(event.agent?.id);
|
|
222
|
+
const eventEpoch = Date.parse(normalizeIsoTimestamp(event.ts, requestTs)) || 0;
|
|
223
|
+
if (eventEpoch <= requestEpoch) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
if (!eventAgentId || eventAgentId === SENTI_IDENTITY.id || eventAgentId === requester) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function normalizeUsageNumber(value) {
|
|
234
|
+
const normalized = Number(value);
|
|
235
|
+
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
return normalized;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildStreamContextDocuments(events = []) {
|
|
242
|
+
return (events || [])
|
|
243
|
+
.map((event, index) => {
|
|
244
|
+
const payload = event && typeof event.payload === "object" ? event.payload : {};
|
|
245
|
+
const text = [
|
|
246
|
+
normalizeString(event.event),
|
|
247
|
+
normalizeString(event.agent?.id || event.agentId),
|
|
248
|
+
normalizeString(payload.message),
|
|
249
|
+
normalizeString(payload.response),
|
|
250
|
+
normalizeString(payload.alert),
|
|
251
|
+
normalizeString(payload.reason),
|
|
252
|
+
normalizeString(payload.file),
|
|
253
|
+
]
|
|
254
|
+
.filter(Boolean)
|
|
255
|
+
.join(" ")
|
|
256
|
+
.trim();
|
|
257
|
+
if (!text) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
documentId: `stream:${index + 1}:${normalizeIsoTimestamp(event.ts, new Date().toISOString())}`,
|
|
262
|
+
sourceType: "session-stream",
|
|
263
|
+
sourcePath: "",
|
|
264
|
+
severity: "P3",
|
|
265
|
+
updatedAt: normalizeIsoTimestamp(event.ts, new Date().toISOString()),
|
|
266
|
+
text,
|
|
267
|
+
metadata: {
|
|
268
|
+
category: "session-stream",
|
|
269
|
+
event: normalizeString(event.event),
|
|
270
|
+
agentId: normalizeString(event.agent?.id || event.agentId),
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
})
|
|
274
|
+
.filter(Boolean);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function loadLatestBlackboardEntries(targetPath, { limit = HELP_BLACKBOARD_ENTRY_LIMIT } = {}) {
|
|
278
|
+
const memoryDirectory = path.join(targetPath, ".sentinelayer", "memory");
|
|
279
|
+
let entries = [];
|
|
280
|
+
try {
|
|
281
|
+
entries = await fsp.readdir(memoryDirectory, { withFileTypes: true });
|
|
282
|
+
} catch {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const files = entries
|
|
287
|
+
.filter((entry) => entry.isFile() && entry.name.startsWith("blackboard-") && entry.name.endsWith(".json"))
|
|
288
|
+
.map((entry) => entry.name)
|
|
289
|
+
.sort((left, right) => right.localeCompare(left));
|
|
290
|
+
for (const fileName of files) {
|
|
291
|
+
const filePath = path.join(memoryDirectory, fileName);
|
|
292
|
+
try {
|
|
293
|
+
const payload = JSON.parse(await fsp.readFile(filePath, "utf-8"));
|
|
294
|
+
if (!Array.isArray(payload.entries)) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
return payload.entries.slice(-Math.max(1, Math.floor(Number(limit) || HELP_BLACKBOARD_ENTRY_LIMIT)));
|
|
298
|
+
} catch {
|
|
299
|
+
// Ignore malformed artifacts and continue searching older files.
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function buildFallbackHelpResponse({ requestMessage = "", synopsis = "context unavailable", contextHints = [] } = {}) {
|
|
306
|
+
const topHints = contextHints.slice(0, 2).join(" | ");
|
|
307
|
+
if (topHints) {
|
|
308
|
+
return `I saw your help_request ("${requestMessage}"). Quick context: ${synopsis}. Top hints: ${topHints}. Share the failing file or stack frame and I can route next steps.`;
|
|
309
|
+
}
|
|
310
|
+
return `I saw your help_request ("${requestMessage}"). Quick context: ${synopsis}. Share the failing file or stack frame and I can route next steps.`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function runWithTimeout(promise, timeoutMs, timeoutMessage) {
|
|
314
|
+
let timeoutHandle = null;
|
|
315
|
+
try {
|
|
316
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
317
|
+
timeoutHandle = setTimeout(() => {
|
|
318
|
+
reject(new Error(timeoutMessage));
|
|
319
|
+
}, timeoutMs);
|
|
320
|
+
if (typeof timeoutHandle.unref === "function") {
|
|
321
|
+
timeoutHandle.unref();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
325
|
+
} finally {
|
|
326
|
+
if (timeoutHandle) {
|
|
327
|
+
clearTimeout(timeoutHandle);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function buildHelpResponseMessage(
|
|
333
|
+
daemonState,
|
|
334
|
+
requestEvent,
|
|
335
|
+
{
|
|
336
|
+
targetPath = process.cwd(),
|
|
337
|
+
} = {}
|
|
338
|
+
) {
|
|
339
|
+
const requestMessage =
|
|
340
|
+
normalizeString(requestEvent?.payload?.message) ||
|
|
341
|
+
normalizeString(requestEvent?.payload?.request) ||
|
|
342
|
+
"help request received";
|
|
343
|
+
|
|
344
|
+
if (typeof daemonState.helpResponder === "function") {
|
|
345
|
+
const custom = await daemonState.helpResponder({
|
|
346
|
+
daemonState,
|
|
347
|
+
requestEvent,
|
|
348
|
+
targetPath,
|
|
349
|
+
});
|
|
350
|
+
const normalizedCustom = normalizeString(custom);
|
|
351
|
+
if (normalizedCustom) {
|
|
352
|
+
return {
|
|
353
|
+
message: normalizedCustom,
|
|
354
|
+
usage: {
|
|
355
|
+
inputTokens: 0,
|
|
356
|
+
outputTokens: 0,
|
|
357
|
+
costUsd: 0,
|
|
358
|
+
model: daemonState.model,
|
|
359
|
+
provider: "custom-responder",
|
|
360
|
+
latencyMs: 0,
|
|
361
|
+
},
|
|
362
|
+
fallbackPath: false,
|
|
363
|
+
fallbackReason: "",
|
|
364
|
+
contextSignals: {
|
|
365
|
+
documentCount: 0,
|
|
366
|
+
memoryHits: 0,
|
|
367
|
+
blackboardEntries: 0,
|
|
368
|
+
recentEvents: 0,
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const session = await getSession(daemonState.sessionId, { targetPath });
|
|
375
|
+
const synopsis = session ? formatCodebaseSynopsis(session) : "codebase context unavailable";
|
|
376
|
+
const outputRoot = path.join(targetPath, ".sentinelayer");
|
|
377
|
+
|
|
378
|
+
const [recentEvents, blackboardEntries, sharedMemory] = await Promise.all([
|
|
379
|
+
readStream(daemonState.sessionId, {
|
|
380
|
+
targetPath,
|
|
381
|
+
tail: HELP_CONTEXT_EVENT_TAIL,
|
|
382
|
+
}).catch(() => []),
|
|
383
|
+
loadLatestBlackboardEntries(targetPath, {
|
|
384
|
+
limit: HELP_BLACKBOARD_ENTRY_LIMIT,
|
|
385
|
+
}),
|
|
386
|
+
buildSharedMemoryCorpus({
|
|
387
|
+
outputRoot,
|
|
388
|
+
targetPath,
|
|
389
|
+
ingest: session?.codebaseContext || {},
|
|
390
|
+
maxAuditRuns: 2,
|
|
391
|
+
}).catch(() => ({
|
|
392
|
+
documents: [],
|
|
393
|
+
sourceCounts: {},
|
|
394
|
+
})),
|
|
395
|
+
]);
|
|
396
|
+
|
|
397
|
+
const documents = [
|
|
398
|
+
...(sharedMemory.documents || []),
|
|
399
|
+
...buildStreamContextDocuments(recentEvents),
|
|
400
|
+
...buildDocumentsFromBlackboardEntries(blackboardEntries),
|
|
401
|
+
];
|
|
402
|
+
const localIndex = buildLocalHybridIndex(documents);
|
|
403
|
+
const memoryQuery = queryLocalHybridIndex(localIndex, {
|
|
404
|
+
query: requestMessage,
|
|
405
|
+
limit: HELP_CONTEXT_RESULT_LIMIT,
|
|
406
|
+
minScore: 0.05,
|
|
407
|
+
});
|
|
408
|
+
const memoryHits = memoryQuery.results || [];
|
|
409
|
+
const contextHints = memoryHits
|
|
410
|
+
.slice(0, HELP_CONTEXT_RESULT_LIMIT)
|
|
411
|
+
.map((result) => {
|
|
412
|
+
const source = normalizeString(result.sourceType) || "memory";
|
|
413
|
+
const snippet = normalizeString(result.snippet || "").replace(/\s+/g, " ").trim();
|
|
414
|
+
if (!snippet) {
|
|
415
|
+
return "";
|
|
416
|
+
}
|
|
417
|
+
return `${source}: ${snippet}`;
|
|
418
|
+
})
|
|
419
|
+
.filter(Boolean);
|
|
420
|
+
|
|
421
|
+
const systemPrompt = [
|
|
422
|
+
"You are Senti, SentinelLayer's session daemon.",
|
|
423
|
+
"Answer the requesting agent with concise, actionable engineering guidance.",
|
|
424
|
+
"Prioritize concrete next steps and reference available context snippets.",
|
|
425
|
+
"Never invent repository files or runtime behavior.",
|
|
426
|
+
].join(" ");
|
|
427
|
+
const userPrompt = [
|
|
428
|
+
`Agent request: ${requestMessage}`,
|
|
429
|
+
`Codebase synopsis: ${synopsis}`,
|
|
430
|
+
"Context snippets:",
|
|
431
|
+
contextHints.length > 0 ? contextHints.map((line, index) => `${index + 1}. ${line}`).join("\n") : "none",
|
|
432
|
+
"Respond in 2-4 short sentences.",
|
|
433
|
+
].join("\n");
|
|
434
|
+
|
|
435
|
+
const startedAt = Date.now();
|
|
436
|
+
let llmText = "";
|
|
437
|
+
let fallbackPath = false;
|
|
438
|
+
let fallbackReason = "";
|
|
439
|
+
let usage = {
|
|
440
|
+
inputTokens: 0,
|
|
441
|
+
outputTokens: 0,
|
|
442
|
+
costUsd: 0,
|
|
443
|
+
model: daemonState.model,
|
|
444
|
+
provider: "local-fallback",
|
|
445
|
+
latencyMs: 0,
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const llmTimeoutMs = Math.max(
|
|
449
|
+
80,
|
|
450
|
+
Math.min(
|
|
451
|
+
HELP_MODEL_TIMEOUT_MS,
|
|
452
|
+
normalizePositiveInteger(daemonState.helpRequestTimeoutMs, HELP_REQUEST_TIMEOUT_MS) * 2
|
|
453
|
+
)
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
const llmResult = await runWithTimeout(
|
|
458
|
+
Promise.resolve(
|
|
459
|
+
daemonState.llmInvoker({
|
|
460
|
+
model: daemonState.model,
|
|
461
|
+
systemPrompt,
|
|
462
|
+
prompt: userPrompt,
|
|
463
|
+
maxTokens: 320,
|
|
464
|
+
temperature: 0.1,
|
|
465
|
+
})
|
|
466
|
+
),
|
|
467
|
+
llmTimeoutMs,
|
|
468
|
+
"Senti model response timeout."
|
|
469
|
+
);
|
|
470
|
+
llmText = normalizeString(llmResult?.text);
|
|
471
|
+
usage = {
|
|
472
|
+
inputTokens: normalizeUsageNumber(llmResult?.usage?.inputTokens),
|
|
473
|
+
outputTokens: normalizeUsageNumber(llmResult?.usage?.outputTokens),
|
|
474
|
+
costUsd: normalizeUsageNumber(llmResult?.usage?.costUsd),
|
|
475
|
+
model: normalizeString(llmResult?.usage?.model) || daemonState.model,
|
|
476
|
+
provider: normalizeString(llmResult?.usage?.provider) || "sentinelayer",
|
|
477
|
+
latencyMs: normalizeUsageNumber(llmResult?.usage?.latencyMs),
|
|
478
|
+
};
|
|
479
|
+
if (!llmText) {
|
|
480
|
+
fallbackPath = true;
|
|
481
|
+
fallbackReason = "Senti model returned an empty response.";
|
|
482
|
+
}
|
|
483
|
+
} catch (error) {
|
|
484
|
+
fallbackPath = true;
|
|
485
|
+
fallbackReason = normalizeString(error?.message || error) || "Senti model invocation failed.";
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!usage.latencyMs) {
|
|
489
|
+
usage.latencyMs = Math.max(1, Date.now() - startedAt);
|
|
490
|
+
}
|
|
491
|
+
recordLlmUsage({
|
|
492
|
+
sessionId: daemonState.telemetrySessionId,
|
|
493
|
+
inputTokens: usage.inputTokens,
|
|
494
|
+
outputTokens: usage.outputTokens,
|
|
495
|
+
costUsd: usage.costUsd,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const message = llmText ||
|
|
499
|
+
buildFallbackHelpResponse({
|
|
500
|
+
requestMessage,
|
|
501
|
+
synopsis,
|
|
502
|
+
contextHints,
|
|
503
|
+
});
|
|
504
|
+
return {
|
|
505
|
+
message,
|
|
506
|
+
usage,
|
|
507
|
+
fallbackPath,
|
|
508
|
+
fallbackReason,
|
|
509
|
+
contextSignals: {
|
|
510
|
+
documentCount: documents.length,
|
|
511
|
+
memoryHits: memoryHits.length,
|
|
512
|
+
blackboardEntries: blackboardEntries.length,
|
|
513
|
+
recentEvents: recentEvents.length,
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function maybeRespondToHelpRequest(
|
|
519
|
+
daemonState,
|
|
520
|
+
requestEvent,
|
|
521
|
+
{
|
|
522
|
+
targetPath = process.cwd(),
|
|
523
|
+
} = {}
|
|
524
|
+
) {
|
|
525
|
+
const requestId =
|
|
526
|
+
normalizeString(requestEvent.requestId) ||
|
|
527
|
+
normalizeString(requestEvent.payload?.requestId) ||
|
|
528
|
+
`${normalizeIsoTimestamp(requestEvent.ts)}:${normalizeString(requestEvent.agent?.id)}`;
|
|
529
|
+
if (!requestId) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
const hasPeerResponse = await hasHelpResponseFromPeer(daemonState.sessionId, requestEvent, {
|
|
533
|
+
targetPath,
|
|
534
|
+
});
|
|
535
|
+
if (hasPeerResponse) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const response = await buildHelpResponseMessage(daemonState, requestEvent, {
|
|
539
|
+
targetPath,
|
|
540
|
+
});
|
|
541
|
+
const nowIso = new Date().toISOString();
|
|
542
|
+
const responseEvent = await emitSentiEvent(
|
|
543
|
+
daemonState.sessionId,
|
|
544
|
+
"help_response",
|
|
545
|
+
{
|
|
546
|
+
requestId,
|
|
547
|
+
targetAgentId: normalizeString(requestEvent.agent?.id) || null,
|
|
548
|
+
response: response.message,
|
|
549
|
+
sourceEvent: "help_request",
|
|
550
|
+
contextSignals: response.contextSignals,
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
targetPath,
|
|
554
|
+
nowIso,
|
|
555
|
+
}
|
|
556
|
+
);
|
|
557
|
+
await emitSentiEvent(
|
|
558
|
+
daemonState.sessionId,
|
|
559
|
+
"model_span",
|
|
560
|
+
{
|
|
561
|
+
sourceEvent: "help_request",
|
|
562
|
+
requestId,
|
|
563
|
+
model: response.usage.model || daemonState.model,
|
|
564
|
+
provider: response.usage.provider || "sentinelayer",
|
|
565
|
+
inputTokens: response.usage.inputTokens,
|
|
566
|
+
outputTokens: response.usage.outputTokens,
|
|
567
|
+
costUsd: response.usage.costUsd,
|
|
568
|
+
latencyMs: response.usage.latencyMs,
|
|
569
|
+
fallbackPath: Boolean(response.fallbackPath),
|
|
570
|
+
fallbackReason: response.fallbackReason || null,
|
|
571
|
+
contextSignals: response.contextSignals,
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
targetPath,
|
|
575
|
+
nowIso,
|
|
576
|
+
}
|
|
577
|
+
);
|
|
578
|
+
return responseEvent;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function queueHelpResponse(daemonState, requestEvent) {
|
|
582
|
+
if (!daemonState.running) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const requestId =
|
|
586
|
+
normalizeString(requestEvent.requestId) ||
|
|
587
|
+
normalizeString(requestEvent.payload?.requestId) ||
|
|
588
|
+
`${normalizeIsoTimestamp(requestEvent.ts)}:${normalizeString(requestEvent.agent?.id)}`;
|
|
589
|
+
if (!requestId || daemonState.pendingHelpTimers.has(requestId)) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const timer = setTimeout(() => {
|
|
593
|
+
daemonState.pendingHelpTimers.delete(requestId);
|
|
594
|
+
void maybeRespondToHelpRequest(daemonState, requestEvent, {
|
|
595
|
+
targetPath: daemonState.targetPath,
|
|
596
|
+
}).catch(() => {});
|
|
597
|
+
}, daemonState.helpRequestTimeoutMs);
|
|
598
|
+
if (typeof timer.unref === "function") {
|
|
599
|
+
timer.unref();
|
|
600
|
+
}
|
|
601
|
+
daemonState.pendingHelpTimers.set(requestId, timer);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function runHelpWatcher(daemonState) {
|
|
605
|
+
const signal = daemonState.helpAbortController.signal;
|
|
606
|
+
try {
|
|
607
|
+
for await (const event of tailStream(daemonState.sessionId, {
|
|
608
|
+
targetPath: daemonState.targetPath,
|
|
609
|
+
signal,
|
|
610
|
+
since: daemonState.startedAt,
|
|
611
|
+
replayTail: 0,
|
|
612
|
+
pollMs: Math.max(25, Math.min(250, Math.floor(daemonState.helpRequestTimeoutMs / 4))),
|
|
613
|
+
})) {
|
|
614
|
+
if (!daemonState.running) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (normalizeString(event.event) !== "help_request") {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (normalizeString(event.agent?.id) === SENTI_IDENTITY.id) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
queueHelpResponse(daemonState, event);
|
|
624
|
+
}
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (error && typeof error === "object" && error.name === "AbortError") {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function splitFileAndIntent(raw = "") {
|
|
634
|
+
const normalized = normalizeString(raw);
|
|
635
|
+
if (!normalized) {
|
|
636
|
+
return {
|
|
637
|
+
filePath: "",
|
|
638
|
+
intent: "",
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
const separatorMatch = /\s(?:—|–|-)\s/.exec(normalized);
|
|
642
|
+
if (!separatorMatch) {
|
|
643
|
+
return {
|
|
644
|
+
filePath: normalizeString(normalized),
|
|
645
|
+
intent: "",
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
const separatorIndex = Number(separatorMatch.index || 0);
|
|
649
|
+
return {
|
|
650
|
+
filePath: normalizeString(normalized.slice(0, separatorIndex)),
|
|
651
|
+
intent: normalizeString(normalized.slice(separatorIndex + separatorMatch[0].length)),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function parseSessionDirective(event = {}) {
|
|
656
|
+
if (normalizeString(event.event) !== "session_message") {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
const message = normalizeString(event.payload?.message);
|
|
660
|
+
if (!message) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
const directive = /^(lock|unlock)\s*:\s*(.+)$/i.exec(message);
|
|
664
|
+
if (!directive) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
const action = normalizeString(directive[1]).toLowerCase();
|
|
668
|
+
const body = normalizeString(directive[2]);
|
|
669
|
+
const parsed = splitFileAndIntent(body);
|
|
670
|
+
if (!parsed.filePath) {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
action,
|
|
675
|
+
filePath: parsed.filePath,
|
|
676
|
+
intent: parsed.intent,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function maybeHandleSessionDirective(daemonState, event) {
|
|
681
|
+
const agentId = normalizeString(event.agent?.id);
|
|
682
|
+
if (!agentId || agentId === SENTI_IDENTITY.id) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
const nowIso = normalizeIsoTimestamp(event.ts, new Date().toISOString());
|
|
686
|
+
const fileDirective = parseSessionDirective(event);
|
|
687
|
+
if (fileDirective) {
|
|
688
|
+
if (fileDirective.action === "lock") {
|
|
689
|
+
const result = await lockFile(
|
|
690
|
+
daemonState.sessionId,
|
|
691
|
+
agentId,
|
|
692
|
+
fileDirective.filePath,
|
|
693
|
+
{
|
|
694
|
+
intent: fileDirective.intent,
|
|
695
|
+
ttlSeconds: DEFAULT_FILE_LOCK_TTL_SECONDS,
|
|
696
|
+
targetPath: daemonState.targetPath,
|
|
697
|
+
nowIso,
|
|
698
|
+
}
|
|
699
|
+
);
|
|
700
|
+
if (!result.locked) {
|
|
701
|
+
await emitSentiEvent(
|
|
702
|
+
daemonState.sessionId,
|
|
703
|
+
"daemon_alert",
|
|
704
|
+
{
|
|
705
|
+
alert: "file_lock_denied",
|
|
706
|
+
file: result.file || fileDirective.filePath,
|
|
707
|
+
requestedBy: agentId,
|
|
708
|
+
heldBy: result.heldBy || null,
|
|
709
|
+
since: result.since || null,
|
|
710
|
+
suggestion: `${fileDirective.filePath} is locked by ${result.heldBy || "another agent"} (${result.since || "recently"}). Coordinate before editing.`,
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
targetPath: daemonState.targetPath,
|
|
714
|
+
nowIso,
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
return result;
|
|
719
|
+
}
|
|
720
|
+
if (fileDirective.action === "unlock") {
|
|
721
|
+
const result = await unlockFile(
|
|
722
|
+
daemonState.sessionId,
|
|
723
|
+
agentId,
|
|
724
|
+
fileDirective.filePath,
|
|
725
|
+
{
|
|
726
|
+
reason: "session_message_unlock",
|
|
727
|
+
targetPath: daemonState.targetPath,
|
|
728
|
+
nowIso,
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
if (!result.unlocked && result.reason === "held_by_other_agent") {
|
|
732
|
+
await emitSentiEvent(
|
|
733
|
+
daemonState.sessionId,
|
|
734
|
+
"daemon_alert",
|
|
735
|
+
{
|
|
736
|
+
alert: "file_unlock_denied",
|
|
737
|
+
file: result.file || fileDirective.filePath,
|
|
738
|
+
requestedBy: agentId,
|
|
739
|
+
heldBy: result.heldBy || null,
|
|
740
|
+
since: result.since || null,
|
|
741
|
+
suggestion: `${fileDirective.filePath} is locked by ${result.heldBy || "another agent"}. Only the lock holder can release it.`,
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
targetPath: daemonState.targetPath,
|
|
745
|
+
nowIso,
|
|
746
|
+
}
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
return result;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
return await handleTaskDirective(daemonState.sessionId, event, {
|
|
755
|
+
targetPath: daemonState.targetPath,
|
|
756
|
+
nowIso,
|
|
757
|
+
});
|
|
758
|
+
} catch (error) {
|
|
759
|
+
await emitSentiEvent(
|
|
760
|
+
daemonState.sessionId,
|
|
761
|
+
"daemon_alert",
|
|
762
|
+
{
|
|
763
|
+
alert: "task_directive_error",
|
|
764
|
+
requestedBy: agentId,
|
|
765
|
+
reason: normalizeString(error?.message) || "Task directive failed.",
|
|
766
|
+
message: normalizeString(event.payload?.message) || null,
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
targetPath: daemonState.targetPath,
|
|
770
|
+
nowIso,
|
|
771
|
+
}
|
|
772
|
+
);
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async function runSessionDirectiveWatcher(daemonState) {
|
|
778
|
+
const signal = daemonState.helpAbortController.signal;
|
|
779
|
+
try {
|
|
780
|
+
for await (const event of tailStream(daemonState.sessionId, {
|
|
781
|
+
targetPath: daemonState.targetPath,
|
|
782
|
+
signal,
|
|
783
|
+
since: daemonState.startedAt,
|
|
784
|
+
replayTail: 0,
|
|
785
|
+
pollMs: 100,
|
|
786
|
+
})) {
|
|
787
|
+
if (!daemonState.running) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (normalizeString(event.event) !== "session_message") {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
await maybeHandleSessionDirective(daemonState, event);
|
|
794
|
+
}
|
|
795
|
+
} catch (error) {
|
|
796
|
+
if (error && typeof error === "object" && error.name === "AbortError") {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
throw error;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function buildConflictSignature(agentA, agentB, filePath) {
|
|
804
|
+
const pair = [normalizeString(agentA), normalizeString(agentB)].filter(Boolean).sort().join("|");
|
|
805
|
+
return `${pair}::${normalizeString(filePath).replace(/\\/g, "/")}`;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function parseEpoch(value, fallbackIso = new Date().toISOString()) {
|
|
809
|
+
return Date.parse(normalizeIsoTimestamp(value, fallbackIso)) || 0;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function createHealthSummaryBase(nowIso, session, agents) {
|
|
813
|
+
return {
|
|
814
|
+
sessionId: session.sessionId,
|
|
815
|
+
generatedAt: normalizeIsoTimestamp(nowIso, new Date().toISOString()),
|
|
816
|
+
expiresAt: session.expiresAt,
|
|
817
|
+
activeAgentCount: agents.filter((agent) => normalizeString(agent.agentId) !== SENTI_IDENTITY.id).length,
|
|
818
|
+
staleAgents: [],
|
|
819
|
+
conflictAlerts: [],
|
|
820
|
+
renewed: null,
|
|
821
|
+
humanMessages: {
|
|
822
|
+
relayed: 0,
|
|
823
|
+
dropped: 0,
|
|
824
|
+
cursor: null,
|
|
825
|
+
reason: "",
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async function emitStaleAndRecoveryAlerts(
|
|
831
|
+
daemonState,
|
|
832
|
+
summary,
|
|
833
|
+
staleAgents = [],
|
|
834
|
+
nowIso = new Date().toISOString()
|
|
835
|
+
) {
|
|
836
|
+
const staleIds = new Set(staleAgents.map((agent) => normalizeString(agent.agentId)));
|
|
837
|
+
|
|
838
|
+
for (const staleAgent of staleAgents) {
|
|
839
|
+
const staleId = normalizeString(staleAgent.agentId);
|
|
840
|
+
if (!staleId || daemonState.staleAlertedAgents.has(staleId)) {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
daemonState.staleAlertedAgents.add(staleId);
|
|
844
|
+
const alert = await emitSentiEvent(
|
|
845
|
+
daemonState.sessionId,
|
|
846
|
+
"daemon_alert",
|
|
847
|
+
{
|
|
848
|
+
alert: "stuck_detected",
|
|
849
|
+
targetAgentId: staleId,
|
|
850
|
+
idleSeconds: staleAgent.idleSeconds,
|
|
851
|
+
thresholdSeconds: daemonState.staleAgentSeconds,
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
targetPath: daemonState.targetPath,
|
|
855
|
+
nowIso,
|
|
856
|
+
}
|
|
857
|
+
);
|
|
858
|
+
summary.staleAgents.push({
|
|
859
|
+
agentId: staleId,
|
|
860
|
+
idleSeconds: staleAgent.idleSeconds,
|
|
861
|
+
event: alert,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
for (const previousStaleId of [...daemonState.staleAlertedAgents]) {
|
|
866
|
+
if (staleIds.has(previousStaleId)) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
daemonState.staleAlertedAgents.delete(previousStaleId);
|
|
870
|
+
await emitSentiEvent(
|
|
871
|
+
daemonState.sessionId,
|
|
872
|
+
"daemon_alert",
|
|
873
|
+
{
|
|
874
|
+
alert: "stuck_recovered",
|
|
875
|
+
targetAgentId: previousStaleId,
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
targetPath: daemonState.targetPath,
|
|
879
|
+
nowIso,
|
|
880
|
+
}
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async function emitConflictAlerts(
|
|
886
|
+
daemonState,
|
|
887
|
+
summary,
|
|
888
|
+
agents = [],
|
|
889
|
+
nowIso = new Date().toISOString()
|
|
890
|
+
) {
|
|
891
|
+
const nowEpoch = parseEpoch(nowIso, nowIso);
|
|
892
|
+
const staleCutoff = nowEpoch - FILE_CONFLICT_WINDOW_MS * 2;
|
|
893
|
+
|
|
894
|
+
for (const [filePath, record] of daemonState.fileActivity.entries()) {
|
|
895
|
+
if (!record || Number(record.timestamp || 0) < staleCutoff) {
|
|
896
|
+
daemonState.fileActivity.delete(filePath);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
for (const [signature, epoch] of daemonState.conflictAlertAt.entries()) {
|
|
900
|
+
if (Number(epoch || 0) < staleCutoff) {
|
|
901
|
+
daemonState.conflictAlertAt.delete(signature);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
for (const agent of agents) {
|
|
906
|
+
const agentId = normalizeString(agent.agentId);
|
|
907
|
+
if (!agentId || agentId === SENTI_IDENTITY.id) {
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const filePath = normalizeString(agent.file).replace(/\\/g, "/");
|
|
911
|
+
if (!filePath) {
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const activityEpoch = parseEpoch(agent.lastActivityAt, nowIso);
|
|
915
|
+
const previous = daemonState.fileActivity.get(filePath) || null;
|
|
916
|
+
if (previous && previous.agentId !== agentId) {
|
|
917
|
+
const deltaMs = Math.abs(activityEpoch - Number(previous.timestamp || 0));
|
|
918
|
+
if (deltaMs <= FILE_CONFLICT_WINDOW_MS) {
|
|
919
|
+
const signature = buildConflictSignature(previous.agentId, agentId, filePath);
|
|
920
|
+
const lastAlertEpoch = Number(daemonState.conflictAlertAt.get(signature) || 0);
|
|
921
|
+
if (nowEpoch - lastAlertEpoch >= FILE_CONFLICT_WINDOW_MS) {
|
|
922
|
+
const event = await emitSentiEvent(
|
|
923
|
+
daemonState.sessionId,
|
|
924
|
+
"daemon_alert",
|
|
925
|
+
{
|
|
926
|
+
alert: "file_conflict",
|
|
927
|
+
file: filePath,
|
|
928
|
+
agentA: previous.agentId,
|
|
929
|
+
agentB: agentId,
|
|
930
|
+
previousSeenAt: normalizeIsoTimestamp(previous.activityAt, nowIso),
|
|
931
|
+
currentSeenAt: normalizeIsoTimestamp(agent.lastActivityAt, nowIso),
|
|
932
|
+
suggestion: `${previous.agentId} and ${agentId} are touching ${filePath}. Coordinate before editing.`,
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
targetPath: daemonState.targetPath,
|
|
936
|
+
nowIso,
|
|
937
|
+
}
|
|
938
|
+
);
|
|
939
|
+
daemonState.conflictAlertAt.set(signature, nowEpoch);
|
|
940
|
+
summary.conflictAlerts.push({
|
|
941
|
+
file: filePath,
|
|
942
|
+
agentA: previous.agentId,
|
|
943
|
+
agentB: agentId,
|
|
944
|
+
event,
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
daemonState.fileActivity.set(filePath, {
|
|
951
|
+
agentId,
|
|
952
|
+
activityAt: normalizeIsoTimestamp(agent.lastActivityAt, nowIso),
|
|
953
|
+
timestamp: activityEpoch,
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
async function maybeRenewActiveSession(
|
|
959
|
+
daemonState,
|
|
960
|
+
summary,
|
|
961
|
+
session,
|
|
962
|
+
nowIso = new Date().toISOString()
|
|
963
|
+
) {
|
|
964
|
+
const nowEpoch = parseEpoch(nowIso, nowIso);
|
|
965
|
+
const expiryEpoch = parseEpoch(session.expiresAt, nowIso);
|
|
966
|
+
if (!Number.isFinite(expiryEpoch) || expiryEpoch <= nowEpoch) {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (expiryEpoch - nowEpoch > RENEWAL_LEAD_MS) {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const recentSinceIso = new Date(nowEpoch - RENEWAL_WINDOW_MS).toISOString();
|
|
973
|
+
const recentEvents = await readStream(daemonState.sessionId, {
|
|
974
|
+
targetPath: daemonState.targetPath,
|
|
975
|
+
tail: 0,
|
|
976
|
+
since: recentSinceIso,
|
|
977
|
+
});
|
|
978
|
+
if (recentEvents.length <= RENEWAL_THRESHOLD_EVENTS) {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const renewed = await renewSession(daemonState.sessionId, {
|
|
982
|
+
targetPath: daemonState.targetPath,
|
|
983
|
+
});
|
|
984
|
+
summary.renewed = {
|
|
985
|
+
renewalCount: renewed.renewalCount,
|
|
986
|
+
expiresAt: renewed.expiresAt,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async function pollAndRelayHumanMessages(
|
|
991
|
+
daemonState,
|
|
992
|
+
summary,
|
|
993
|
+
nowIso = new Date().toISOString()
|
|
994
|
+
) {
|
|
995
|
+
if (daemonState.humanMessagePollInFlight) {
|
|
996
|
+
summary.humanMessages.reason = "poll_in_flight";
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
daemonState.humanMessagePollInFlight = true;
|
|
1001
|
+
try {
|
|
1002
|
+
const polled = await pollHumanMessages(daemonState.sessionId, {
|
|
1003
|
+
targetPath: daemonState.targetPath,
|
|
1004
|
+
since: daemonState.humanMessageCursor,
|
|
1005
|
+
});
|
|
1006
|
+
if (!polled.ok) {
|
|
1007
|
+
summary.humanMessages.reason = normalizeString(polled.reason) || "poll_failed";
|
|
1008
|
+
summary.humanMessages.cursor = daemonState.humanMessageCursor;
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const relayedEvents = [];
|
|
1013
|
+
for (const event of polled.events || []) {
|
|
1014
|
+
const persisted = await appendToStream(daemonState.sessionId, event, {
|
|
1015
|
+
targetPath: daemonState.targetPath,
|
|
1016
|
+
});
|
|
1017
|
+
relayedEvents.push(persisted);
|
|
1018
|
+
}
|
|
1019
|
+
daemonState.humanMessageCursor = normalizeString(polled.cursor) || daemonState.humanMessageCursor;
|
|
1020
|
+
|
|
1021
|
+
summary.humanMessages.relayed = relayedEvents.length;
|
|
1022
|
+
summary.humanMessages.dropped = Array.isArray(polled.dropped) ? polled.dropped.length : 0;
|
|
1023
|
+
summary.humanMessages.cursor = daemonState.humanMessageCursor;
|
|
1024
|
+
summary.humanMessages.reason = "";
|
|
1025
|
+
|
|
1026
|
+
if (relayedEvents.length > 0) {
|
|
1027
|
+
await emitSentiEvent(
|
|
1028
|
+
daemonState.sessionId,
|
|
1029
|
+
"daemon_alert",
|
|
1030
|
+
{
|
|
1031
|
+
alert: "human_directive_received",
|
|
1032
|
+
relayedCount: relayedEvents.length,
|
|
1033
|
+
droppedCount: summary.humanMessages.dropped,
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
targetPath: daemonState.targetPath,
|
|
1037
|
+
nowIso,
|
|
1038
|
+
}
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
summary.humanMessages.reason =
|
|
1043
|
+
normalizeString(error?.message) || "poll_relay_failed";
|
|
1044
|
+
summary.humanMessages.cursor = daemonState.humanMessageCursor;
|
|
1045
|
+
} finally {
|
|
1046
|
+
daemonState.humanMessagePollInFlight = false;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
export async function runSentiHealthTick(
|
|
1051
|
+
sessionId,
|
|
1052
|
+
{
|
|
1053
|
+
targetPath = process.cwd(),
|
|
1054
|
+
nowIso = new Date().toISOString(),
|
|
1055
|
+
staleAgentSeconds = DEFAULT_STALE_AGENT_SECONDS,
|
|
1056
|
+
daemonState = null,
|
|
1057
|
+
} = {}
|
|
1058
|
+
) {
|
|
1059
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1060
|
+
if (!normalizedSessionId) {
|
|
1061
|
+
throw new Error("sessionId is required.");
|
|
1062
|
+
}
|
|
1063
|
+
const normalizedTargetPath = path.resolve(String(targetPath || "."));
|
|
1064
|
+
const session = await getSession(normalizedSessionId, {
|
|
1065
|
+
targetPath: normalizedTargetPath,
|
|
1066
|
+
});
|
|
1067
|
+
if (!session) {
|
|
1068
|
+
throw new Error(`Session '${normalizedSessionId}' was not found.`);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const resolvedDaemonState =
|
|
1072
|
+
daemonState ||
|
|
1073
|
+
createSentiState({
|
|
1074
|
+
daemonKey: buildDaemonKey(normalizedSessionId, normalizedTargetPath),
|
|
1075
|
+
sessionId: normalizedSessionId,
|
|
1076
|
+
targetPath: normalizedTargetPath,
|
|
1077
|
+
startedAt: normalizeIsoTimestamp(nowIso, nowIso),
|
|
1078
|
+
model: SENTI_MODEL,
|
|
1079
|
+
staleAgentSeconds,
|
|
1080
|
+
helpRequestTimeoutMs: HELP_REQUEST_TIMEOUT_MS,
|
|
1081
|
+
tickIntervalMs: DAEMON_TICK_INTERVAL_MS,
|
|
1082
|
+
recapIntervalMs: DEFAULT_RECAP_INTERVAL_MS_OVERRIDE,
|
|
1083
|
+
recapInactivityMs: DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE,
|
|
1084
|
+
helpResponder: null,
|
|
1085
|
+
llmInvoker: invokeViaProxy,
|
|
1086
|
+
telemetrySessionId: null,
|
|
1087
|
+
});
|
|
1088
|
+
const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
|
|
1089
|
+
const activeAgents = await listAgents(normalizedSessionId, {
|
|
1090
|
+
targetPath: normalizedTargetPath,
|
|
1091
|
+
includeInactive: false,
|
|
1092
|
+
});
|
|
1093
|
+
const filteredAgents = activeAgents.filter((agent) => normalizeString(agent.agentId) !== SENTI_IDENTITY.id);
|
|
1094
|
+
const staleAgents = detectStaleAgents(filteredAgents, {
|
|
1095
|
+
idleThresholdSeconds: normalizePositiveInteger(
|
|
1096
|
+
staleAgentSeconds,
|
|
1097
|
+
normalizePositiveInteger(resolvedDaemonState.staleAgentSeconds, DEFAULT_STALE_AGENT_SECONDS)
|
|
1098
|
+
),
|
|
1099
|
+
nowIso: normalizedNow,
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
const summary = createHealthSummaryBase(normalizedNow, session, activeAgents);
|
|
1103
|
+
await emitStaleAndRecoveryAlerts(resolvedDaemonState, summary, staleAgents, normalizedNow);
|
|
1104
|
+
await emitConflictAlerts(resolvedDaemonState, summary, filteredAgents, normalizedNow);
|
|
1105
|
+
await maybeRenewActiveSession(resolvedDaemonState, summary, session, normalizedNow);
|
|
1106
|
+
await pollAndRelayHumanMessages(resolvedDaemonState, summary, normalizedNow);
|
|
1107
|
+
return summary;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
export async function startSenti(
|
|
1111
|
+
sessionId,
|
|
1112
|
+
{
|
|
1113
|
+
model = SENTI_MODEL,
|
|
1114
|
+
targetPath = process.cwd(),
|
|
1115
|
+
autoStart = true,
|
|
1116
|
+
tickIntervalMs = DAEMON_TICK_INTERVAL_MS,
|
|
1117
|
+
staleAgentSeconds = DEFAULT_STALE_AGENT_SECONDS,
|
|
1118
|
+
helpRequestTimeoutMs = HELP_REQUEST_TIMEOUT_MS,
|
|
1119
|
+
recapIntervalMs = DEFAULT_RECAP_INTERVAL_MS_OVERRIDE,
|
|
1120
|
+
recapInactivityMs = DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE,
|
|
1121
|
+
helpResponder = null,
|
|
1122
|
+
llmInvoker = invokeViaProxy,
|
|
1123
|
+
} = {}
|
|
1124
|
+
) {
|
|
1125
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1126
|
+
if (!normalizedSessionId) {
|
|
1127
|
+
throw new Error("sessionId is required.");
|
|
1128
|
+
}
|
|
1129
|
+
const normalizedTargetPath = path.resolve(String(targetPath || "."));
|
|
1130
|
+
const daemonKey = buildDaemonKey(normalizedSessionId, normalizedTargetPath);
|
|
1131
|
+
const existing = ACTIVE_SENTI_DAEMONS.get(daemonKey);
|
|
1132
|
+
if (existing && existing.running) {
|
|
1133
|
+
return existing.handle;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const session = await getSession(normalizedSessionId, {
|
|
1137
|
+
targetPath: normalizedTargetPath,
|
|
1138
|
+
});
|
|
1139
|
+
if (!session) {
|
|
1140
|
+
throw new Error(`Session '${normalizedSessionId}' was not found.`);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const normalizedTickIntervalMs = normalizePositiveInteger(tickIntervalMs, DAEMON_TICK_INTERVAL_MS);
|
|
1144
|
+
const normalizedHelpTimeoutMs = normalizePositiveInteger(
|
|
1145
|
+
helpRequestTimeoutMs,
|
|
1146
|
+
HELP_REQUEST_TIMEOUT_MS
|
|
1147
|
+
);
|
|
1148
|
+
const normalizedStaleSeconds = normalizePositiveInteger(
|
|
1149
|
+
staleAgentSeconds,
|
|
1150
|
+
DEFAULT_STALE_AGENT_SECONDS
|
|
1151
|
+
);
|
|
1152
|
+
const normalizedRecapIntervalMs = normalizePositiveInteger(
|
|
1153
|
+
recapIntervalMs,
|
|
1154
|
+
DEFAULT_RECAP_INTERVAL_MS_OVERRIDE
|
|
1155
|
+
);
|
|
1156
|
+
const normalizedRecapInactivityMs = normalizePositiveInteger(
|
|
1157
|
+
recapInactivityMs,
|
|
1158
|
+
DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE
|
|
1159
|
+
);
|
|
1160
|
+
const nowIso = new Date().toISOString();
|
|
1161
|
+
const telemetrySession = startTelemetrySession(`session daemon ${normalizedSessionId}`);
|
|
1162
|
+
const daemonState = createSentiState({
|
|
1163
|
+
daemonKey,
|
|
1164
|
+
sessionId: normalizedSessionId,
|
|
1165
|
+
targetPath: normalizedTargetPath,
|
|
1166
|
+
startedAt: nowIso,
|
|
1167
|
+
model: normalizeString(model) || SENTI_MODEL,
|
|
1168
|
+
staleAgentSeconds: normalizedStaleSeconds,
|
|
1169
|
+
helpRequestTimeoutMs: normalizedHelpTimeoutMs,
|
|
1170
|
+
tickIntervalMs: normalizedTickIntervalMs,
|
|
1171
|
+
recapIntervalMs: normalizedRecapIntervalMs,
|
|
1172
|
+
recapInactivityMs: normalizedRecapInactivityMs,
|
|
1173
|
+
helpResponder,
|
|
1174
|
+
llmInvoker: typeof llmInvoker === "function" ? llmInvoker : invokeViaProxy,
|
|
1175
|
+
telemetrySessionId: telemetrySession?.id || null,
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
await upsertSentiAgent(normalizedSessionId, {
|
|
1179
|
+
targetPath: normalizedTargetPath,
|
|
1180
|
+
model: daemonState.model,
|
|
1181
|
+
});
|
|
1182
|
+
const activeAgents = await listAgents(normalizedSessionId, {
|
|
1183
|
+
targetPath: normalizedTargetPath,
|
|
1184
|
+
includeInactive: false,
|
|
1185
|
+
});
|
|
1186
|
+
await emitSentiEvent(
|
|
1187
|
+
normalizedSessionId,
|
|
1188
|
+
"daemon_alert",
|
|
1189
|
+
{
|
|
1190
|
+
alert: "senti_online",
|
|
1191
|
+
model: daemonState.model,
|
|
1192
|
+
message: buildWelcomeMessage(session, activeAgents),
|
|
1193
|
+
codebaseSynopsis: formatCodebaseSynopsis(session),
|
|
1194
|
+
activeAgents: activeAgents
|
|
1195
|
+
.filter((agent) => normalizeString(agent.agentId) !== SENTI_IDENTITY.id)
|
|
1196
|
+
.map((agent) => ({
|
|
1197
|
+
agentId: agent.agentId,
|
|
1198
|
+
status: agent.status,
|
|
1199
|
+
role: agent.role,
|
|
1200
|
+
})),
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
targetPath: normalizedTargetPath,
|
|
1204
|
+
nowIso,
|
|
1205
|
+
}
|
|
1206
|
+
);
|
|
1207
|
+
|
|
1208
|
+
const runTick = async (tickNowIso = new Date().toISOString()) => {
|
|
1209
|
+
if (!daemonState.running) {
|
|
1210
|
+
return daemonState.lastTickSummary;
|
|
1211
|
+
}
|
|
1212
|
+
const summary = await runSentiHealthTick(normalizedSessionId, {
|
|
1213
|
+
targetPath: normalizedTargetPath,
|
|
1214
|
+
nowIso: tickNowIso,
|
|
1215
|
+
staleAgentSeconds: daemonState.staleAgentSeconds,
|
|
1216
|
+
daemonState,
|
|
1217
|
+
});
|
|
1218
|
+
daemonState.lastTickAt = normalizeIsoTimestamp(tickNowIso, new Date().toISOString());
|
|
1219
|
+
daemonState.lastTickSummary = summary;
|
|
1220
|
+
return summary;
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
const stop = async (reason = "manual_stop") => {
|
|
1224
|
+
if (!daemonState.running) {
|
|
1225
|
+
return {
|
|
1226
|
+
stopped: false,
|
|
1227
|
+
daemonKey,
|
|
1228
|
+
reason: normalizeString(reason) || "manual_stop",
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
daemonState.running = false;
|
|
1233
|
+
if (daemonState.tickTimer) {
|
|
1234
|
+
clearInterval(daemonState.tickTimer);
|
|
1235
|
+
daemonState.tickTimer = null;
|
|
1236
|
+
}
|
|
1237
|
+
daemonState.helpAbortController.abort();
|
|
1238
|
+
for (const timer of daemonState.pendingHelpTimers.values()) {
|
|
1239
|
+
clearTimeout(timer);
|
|
1240
|
+
}
|
|
1241
|
+
daemonState.pendingHelpTimers.clear();
|
|
1242
|
+
if (daemonState.recapEmitter && daemonState.recapEmitter.isRunning()) {
|
|
1243
|
+
daemonState.recapEmitter.stop("daemon_stop");
|
|
1244
|
+
daemonState.recapEmitter = null;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
let runtimeStopSummary = null;
|
|
1248
|
+
try {
|
|
1249
|
+
runtimeStopSummary = await stopRuntimeRunsForSession(normalizedSessionId, {
|
|
1250
|
+
targetPath: normalizedTargetPath,
|
|
1251
|
+
reason: "manual_stop",
|
|
1252
|
+
});
|
|
1253
|
+
} catch {
|
|
1254
|
+
runtimeStopSummary = {
|
|
1255
|
+
sessionId: normalizedSessionId,
|
|
1256
|
+
targetPath: normalizedTargetPath,
|
|
1257
|
+
stoppedCount: 0,
|
|
1258
|
+
runs: [],
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
try {
|
|
1263
|
+
await unregisterAgent(normalizedSessionId, SENTI_IDENTITY.id, {
|
|
1264
|
+
reason: "killed",
|
|
1265
|
+
targetPath: normalizedTargetPath,
|
|
1266
|
+
});
|
|
1267
|
+
} catch {
|
|
1268
|
+
// Non-blocking: if snapshot is already gone, continue to emit explicit kill event.
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const killedEvent = await emitSentiEvent(
|
|
1272
|
+
normalizedSessionId,
|
|
1273
|
+
"agent_killed",
|
|
1274
|
+
{
|
|
1275
|
+
target: SENTI_IDENTITY.id,
|
|
1276
|
+
reason: normalizeString(reason) || "manual_stop",
|
|
1277
|
+
runtimeStops: runtimeStopSummary?.stoppedCount || 0,
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
targetPath: normalizedTargetPath,
|
|
1281
|
+
nowIso: new Date().toISOString(),
|
|
1282
|
+
}
|
|
1283
|
+
);
|
|
1284
|
+
ACTIVE_SENTI_DAEMONS.delete(daemonKey);
|
|
1285
|
+
if (daemonState.telemetrySessionId) {
|
|
1286
|
+
endTelemetrySession({ sessionId: daemonState.telemetrySessionId });
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
stopped: true,
|
|
1290
|
+
daemonKey,
|
|
1291
|
+
sessionId: normalizedSessionId,
|
|
1292
|
+
targetPath: normalizedTargetPath,
|
|
1293
|
+
reason: normalizeString(reason) || "manual_stop",
|
|
1294
|
+
runtimeStopSummary,
|
|
1295
|
+
event: killedEvent,
|
|
1296
|
+
};
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
const handle = {
|
|
1300
|
+
daemonKey,
|
|
1301
|
+
sessionId: normalizedSessionId,
|
|
1302
|
+
targetPath: normalizedTargetPath,
|
|
1303
|
+
startedAt: nowIso,
|
|
1304
|
+
model: daemonState.model,
|
|
1305
|
+
runTick,
|
|
1306
|
+
stop,
|
|
1307
|
+
isRunning: () => daemonState.running,
|
|
1308
|
+
getState: () => ({
|
|
1309
|
+
daemonKey,
|
|
1310
|
+
sessionId: normalizedSessionId,
|
|
1311
|
+
targetPath: normalizedTargetPath,
|
|
1312
|
+
startedAt: nowIso,
|
|
1313
|
+
running: daemonState.running,
|
|
1314
|
+
lastTickAt: daemonState.lastTickAt,
|
|
1315
|
+
staleAlertedAgents: [...daemonState.staleAlertedAgents],
|
|
1316
|
+
pendingHelpRequests: daemonState.pendingHelpTimers.size,
|
|
1317
|
+
recapRunning: Boolean(daemonState.recapEmitter?.isRunning?.()),
|
|
1318
|
+
humanMessageCursor: daemonState.humanMessageCursor,
|
|
1319
|
+
}),
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1322
|
+
daemonState.handle = handle;
|
|
1323
|
+
ACTIVE_SENTI_DAEMONS.set(daemonKey, daemonState);
|
|
1324
|
+
|
|
1325
|
+
void runHelpWatcher(daemonState).catch(() => {});
|
|
1326
|
+
void runSessionDirectiveWatcher(daemonState).catch(() => {});
|
|
1327
|
+
daemonState.recapEmitter = emitPeriodicRecap(normalizedSessionId, {
|
|
1328
|
+
targetPath: normalizedTargetPath,
|
|
1329
|
+
intervalMs: daemonState.recapIntervalMs,
|
|
1330
|
+
inactivityMs: daemonState.recapInactivityMs,
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
if (autoStart) {
|
|
1334
|
+
await runTick(nowIso);
|
|
1335
|
+
daemonState.tickTimer = setInterval(() => {
|
|
1336
|
+
void runTick(new Date().toISOString()).catch(() => {});
|
|
1337
|
+
}, normalizedTickIntervalMs);
|
|
1338
|
+
if (typeof daemonState.tickTimer.unref === "function") {
|
|
1339
|
+
daemonState.tickTimer.unref();
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
return handle;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
export async function stopSenti(
|
|
1347
|
+
sessionId,
|
|
1348
|
+
{
|
|
1349
|
+
targetPath = process.cwd(),
|
|
1350
|
+
reason = "manual_stop",
|
|
1351
|
+
} = {}
|
|
1352
|
+
) {
|
|
1353
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1354
|
+
if (!normalizedSessionId) {
|
|
1355
|
+
throw new Error("sessionId is required.");
|
|
1356
|
+
}
|
|
1357
|
+
const normalizedTargetPath = path.resolve(String(targetPath || "."));
|
|
1358
|
+
const daemonKey = buildDaemonKey(normalizedSessionId, normalizedTargetPath);
|
|
1359
|
+
const daemonState = ACTIVE_SENTI_DAEMONS.get(daemonKey);
|
|
1360
|
+
if (!daemonState || !daemonState.running) {
|
|
1361
|
+
return {
|
|
1362
|
+
stopped: false,
|
|
1363
|
+
daemonKey,
|
|
1364
|
+
sessionId: normalizedSessionId,
|
|
1365
|
+
targetPath: normalizedTargetPath,
|
|
1366
|
+
reason: normalizeString(reason) || "manual_stop",
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
return daemonState.handle.stop(reason);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
export function getSentiDaemon(
|
|
1373
|
+
sessionId,
|
|
1374
|
+
{
|
|
1375
|
+
targetPath = process.cwd(),
|
|
1376
|
+
} = {}
|
|
1377
|
+
) {
|
|
1378
|
+
const daemonKey = buildDaemonKey(sessionId, targetPath);
|
|
1379
|
+
const daemonState = ACTIVE_SENTI_DAEMONS.get(daemonKey);
|
|
1380
|
+
return daemonState ? daemonState.handle : null;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
export {
|
|
1384
|
+
ACTIVE_SENTI_DAEMONS,
|
|
1385
|
+
DAEMON_TICK_INTERVAL_MS,
|
|
1386
|
+
DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE,
|
|
1387
|
+
DEFAULT_RECAP_INTERVAL_MS_OVERRIDE,
|
|
1388
|
+
DEFAULT_STALE_AGENT_SECONDS,
|
|
1389
|
+
FILE_CONFLICT_WINDOW_MS,
|
|
1390
|
+
HELP_REQUEST_TIMEOUT_MS,
|
|
1391
|
+
RENEWAL_LEAD_MS,
|
|
1392
|
+
RENEWAL_THRESHOLD_EVENTS,
|
|
1393
|
+
RENEWAL_WINDOW_MS,
|
|
1394
|
+
SENTI_IDENTITY,
|
|
1395
|
+
SENTI_MODEL,
|
|
1396
|
+
};
|